@donkeylabs/server 0.3.0 → 0.4.0

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 (49) 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 +8 -11
  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 +566 -0
  30. package/src/generator/zod-to-ts.ts +114 -0
  31. package/src/handlers.ts +14 -110
  32. package/src/index.ts +30 -24
  33. package/src/middleware.ts +2 -5
  34. package/src/registry.ts +4 -0
  35. package/src/router.ts +47 -1
  36. package/src/server.ts +618 -332
  37. package/README.md +0 -254
  38. package/cli/commands/dev.ts +0 -134
  39. package/cli/commands/generate.ts +0 -605
  40. package/cli/commands/init.ts +0 -205
  41. package/cli/commands/interactive.ts +0 -417
  42. package/cli/commands/plugin.ts +0 -192
  43. package/cli/commands/route.ts +0 -195
  44. package/cli/donkeylabs +0 -2
  45. package/cli/index.ts +0 -114
  46. package/docs/svelte-frontend.md +0 -324
  47. package/docs/testing.md +0 -438
  48. package/mcp/donkeylabs-mcp +0 -3238
  49. package/mcp/server.ts +0 -3238
@@ -19,8 +19,6 @@ export interface LoggerConfig {
19
19
  level?: LogLevel;
20
20
  transports?: LogTransport[];
21
21
  format?: "json" | "pretty";
22
- /** Timezone for timestamps (e.g., "America/New_York", "UTC"). Defaults to local timezone. */
23
- timezone?: string;
24
22
  }
25
23
 
26
24
  export interface Logger {
@@ -49,86 +47,27 @@ const RESET = "\x1b[0m";
49
47
 
50
48
  // Console transport with pretty or JSON formatting
51
49
  export class ConsoleTransport implements LogTransport {
52
- private format: "json" | "pretty";
53
- private timezone?: string;
54
-
55
- constructor(format: "json" | "pretty" = "pretty", timezone?: string) {
56
- this.format = format;
57
- this.timezone = timezone;
58
- }
59
-
60
- private formatTimestamp(date: Date): string {
61
- if (this.timezone) {
62
- try {
63
- return date.toLocaleString("en-US", {
64
- timeZone: this.timezone,
65
- hour12: false,
66
- hour: "2-digit",
67
- minute: "2-digit",
68
- second: "2-digit",
69
- fractionalSecondDigits: 3,
70
- } as Intl.DateTimeFormatOptions);
71
- } catch {
72
- // Invalid timezone, fall back to ISO
73
- return date.toISOString().slice(11, 23);
74
- }
75
- }
76
- return date.toISOString().slice(11, 23);
77
- }
78
-
79
- private formatTimezone(): string {
80
- if (!this.timezone) return "";
81
- // Get short timezone abbreviation
82
- try {
83
- const tzPart = new Date().toLocaleString("en-US", {
84
- timeZone: this.timezone,
85
- timeZoneName: "short",
86
- }).split(" ").pop();
87
- return ` ${tzPart}`;
88
- } catch {
89
- return "";
90
- }
91
- }
50
+ constructor(private format: "json" | "pretty" = "pretty") {}
92
51
 
93
52
  log(entry: LogEntry): void {
94
53
  if (this.format === "json") {
95
54
  console.log(JSON.stringify({
96
55
  timestamp: entry.timestamp.toISOString(),
97
56
  level: entry.level,
98
- ...(entry.context?.plugin ? { plugin: entry.context.plugin } : {}),
99
57
  message: entry.message,
100
58
  ...entry.data,
101
- // Include other context fields except plugin (already extracted)
102
- ...(entry.context ? Object.fromEntries(
103
- Object.entries(entry.context).filter(([k]) => k !== "plugin")
104
- ) : {}),
59
+ ...entry.context,
105
60
  }));
106
61
  } else {
107
- const time = this.formatTimestamp(entry.timestamp);
108
- const tz = this.formatTimezone();
109
- const level = entry.level.toUpperCase();
110
- const levelColor = LEVEL_COLORS[entry.level];
111
-
112
- // Extract plugin from context for prefix display
113
- const plugin = entry.context?.plugin;
114
-
115
- // Build prefix: [timestamp tz][plugin][LEVEL]
116
- const timePart = `\x1b[90m[${time}${tz}]${RESET}`;
117
- const pluginPart = plugin ? `\x1b[35m[${plugin}]${RESET}` : "";
118
- const levelPart = `${levelColor}[${level}]${RESET}`;
119
-
120
- let output = `${timePart}${pluginPart}${levelPart} ${entry.message}`;
121
-
122
- // Show data as key=value pairs (excluding plugin from context since it's in prefix)
123
- const contextWithoutPlugin = entry.context
124
- ? Object.fromEntries(Object.entries(entry.context).filter(([k]) => k !== "plugin"))
125
- : {};
126
- const extra = { ...entry.data, ...contextWithoutPlugin };
62
+ const color = LEVEL_COLORS[entry.level];
63
+ const time = entry.timestamp.toISOString().slice(11, 23);
64
+ const level = entry.level.toUpperCase().padEnd(5);
65
+
66
+ let output = `${color}[${time}] ${level}${RESET} ${entry.message}`;
67
+
68
+ const extra = { ...entry.data, ...entry.context };
127
69
  if (Object.keys(extra).length > 0) {
128
- const pairs = Object.entries(extra)
129
- .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
130
- .join(" ");
131
- output += ` ${"\x1b[90m"}${pairs}${RESET}`;
70
+ output += ` ${"\x1b[90m"}${JSON.stringify(extra)}${RESET}`;
132
71
  }
133
72
 
134
73
  console.log(output);
@@ -140,12 +79,10 @@ class LoggerImpl implements Logger {
140
79
  private minLevel: number;
141
80
  private transports: LogTransport[];
142
81
  private context: Record<string, any>;
143
- private config: LoggerConfig;
144
82
 
145
83
  constructor(config: LoggerConfig = {}, context: Record<string, any> = {}) {
146
- this.config = config;
147
84
  this.minLevel = LOG_LEVELS[config.level ?? "info"];
148
- this.transports = config.transports ?? [new ConsoleTransport(config.format ?? "pretty", config.timezone)];
85
+ this.transports = config.transports ?? [new ConsoleTransport(config.format ?? "pretty")];
149
86
  this.context = context;
150
87
  }
151
88
 
@@ -183,11 +120,7 @@ class LoggerImpl implements Logger {
183
120
 
184
121
  child(context: Record<string, any>): Logger {
185
122
  return new LoggerImpl(
186
- {
187
- ...this.config,
188
- level: Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k as LogLevel] === this.minLevel) as LogLevel,
189
- transports: this.transports,
190
- },
123
+ { level: Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k as LogLevel] === this.minLevel) as LogLevel, transports: this.transports },
191
124
  { ...this.context, ...context }
192
125
  );
193
126
  }
@@ -19,34 +19,9 @@ export interface RateLimiterConfig {
19
19
  adapter?: RateLimitAdapter;
20
20
  }
21
21
 
22
- export interface RateLimitRule {
23
- /** Max requests allowed in window */
24
- limit: number;
25
- /** Time window (supports "10s", "1m", "1h" or milliseconds) */
26
- window: string | number;
27
- /** Custom key extraction (defaults to IP + route) */
28
- keyFn?: (ctx: { ip: string; route: string; user?: any }) => string;
29
- /** Skip rate limiting for certain conditions */
30
- skip?: (ctx: { ip: string; route: string; user?: any }) => boolean;
31
- /** Custom error message when rate limited */
32
- message?: string;
33
- }
34
-
35
22
  export interface RateLimiter {
36
- // Manual rate limiting
37
23
  check(key: string, limit: number, windowMs: number): Promise<RateLimitResult>;
38
24
  reset(key: string): Promise<void>;
39
-
40
- // Declarative rule registration
41
- registerRule(pattern: string, rule: RateLimitRule): void;
42
- registerRules(rules: Record<string, RateLimitRule>): void;
43
- getRule(route: string): { pattern: string; rule: RateLimitRule } | undefined;
44
- matchRoute(route: string): RateLimitRule | undefined;
45
- /** List all registered rules */
46
- listRules(): Array<{ pattern: string; limit: number; window: string | number }>;
47
-
48
- /** Check a route against registered rules */
49
- checkRoute(route: string, ctx: { ip: string; user?: any }): Promise<RateLimitResult | null>;
50
25
  }
51
26
 
52
27
  // In-memory rate limit adapter using sliding window
@@ -105,90 +80,11 @@ export class MemoryRateLimitAdapter implements RateLimitAdapter {
105
80
 
106
81
  class RateLimiterImpl implements RateLimiter {
107
82
  private adapter: RateLimitAdapter;
108
- private rules = new Map<string, RateLimitRule>();
109
- private rulePatterns: Array<{ pattern: string; regex: RegExp; rule: RateLimitRule }> = [];
110
83
 
111
84
  constructor(config: RateLimiterConfig = {}) {
112
85
  this.adapter = config.adapter ?? new MemoryRateLimitAdapter();
113
86
  }
114
87
 
115
- registerRule(pattern: string, rule: RateLimitRule): void {
116
- this.rules.set(pattern, rule);
117
-
118
- // Pre-compile regex for patterns with wildcards
119
- if (pattern.includes("*")) {
120
- const regex = new RegExp(
121
- "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
122
- );
123
- this.rulePatterns.push({ pattern, regex, rule });
124
- }
125
- }
126
-
127
- registerRules(rules: Record<string, RateLimitRule>): void {
128
- for (const [pattern, rule] of Object.entries(rules)) {
129
- this.registerRule(pattern, rule);
130
- }
131
- }
132
-
133
- getRule(route: string): { pattern: string; rule: RateLimitRule } | undefined {
134
- // Exact match first
135
- const exact = this.rules.get(route);
136
- if (exact && !route.includes("*")) {
137
- return { pattern: route, rule: exact };
138
- }
139
-
140
- // Pattern match (most specific first - by length)
141
- const matches = this.rulePatterns
142
- .filter(({ regex }) => regex.test(route))
143
- .sort((a, b) => b.pattern.length - a.pattern.length);
144
-
145
- const first = matches[0];
146
- if (first) {
147
- return { pattern: first.pattern, rule: first.rule };
148
- }
149
-
150
- return undefined;
151
- }
152
-
153
- matchRoute(route: string): RateLimitRule | undefined {
154
- return this.getRule(route)?.rule;
155
- }
156
-
157
- listRules(): Array<{ pattern: string; limit: number; window: string | number }> {
158
- return Array.from(this.rules.entries()).map(([pattern, rule]) => ({
159
- pattern,
160
- limit: rule.limit,
161
- window: rule.window,
162
- }));
163
- }
164
-
165
- async checkRoute(
166
- route: string,
167
- ctx: { ip: string; user?: any }
168
- ): Promise<RateLimitResult | null> {
169
- const match = this.getRule(route);
170
- if (!match) return null;
171
-
172
- const { rule } = match;
173
-
174
- // Check skip condition
175
- if (rule.skip?.({ ip: ctx.ip, route, user: ctx.user })) {
176
- return null;
177
- }
178
-
179
- // Generate key
180
- const key = rule.keyFn
181
- ? rule.keyFn({ ip: ctx.ip, route, user: ctx.user })
182
- : createRateLimitKey(route, ctx.ip);
183
-
184
- // Parse window
185
- const windowMs = typeof rule.window === "string"
186
- ? parseDuration(rule.window)
187
- : rule.window;
188
-
189
- return this.check(key, rule.limit, windowMs);
190
- }
191
-
192
88
  async check(key: string, limit: number, windowMs: number): Promise<RateLimitResult> {
193
89
  const { count, resetAt } = await this.adapter.increment(key, windowMs);
194
90
 
@@ -218,7 +114,18 @@ export function createRateLimiter(config?: RateLimiterConfig): RateLimiter {
218
114
  return new RateLimiterImpl(config);
219
115
  }
220
116
 
117
+ // ==========================================
221
118
  // IP Extraction Utilities
119
+ // ==========================================
120
+
121
+ /**
122
+ * Priority order for IP detection headers:
123
+ * 1. CF-Connecting-IP (Cloudflare)
124
+ * 2. True-Client-IP (Akamai, Cloudflare Enterprise)
125
+ * 3. X-Real-IP (Nginx)
126
+ * 4. X-Forwarded-For (first IP in chain)
127
+ * 5. Request socket address (direct connection)
128
+ */
222
129
  const IP_HEADERS = [
223
130
  "cf-connecting-ip",
224
131
  "true-client-ip",
@@ -226,7 +133,10 @@ const IP_HEADERS = [
226
133
  "x-forwarded-for",
227
134
  ] as const;
228
135
 
229
- /** Extract client IP address from request headers (handles Cloudflare, Nginx, etc.) */
136
+ /**
137
+ * Extract client IP address from request headers
138
+ * Handles various proxy configurations (Cloudflare, Nginx, etc.)
139
+ */
230
140
  export function extractClientIP(req: Request, socketAddr?: string): string {
231
141
  for (const header of IP_HEADERS) {
232
142
  const value = req.headers.get(header);
@@ -249,7 +159,9 @@ export function extractClientIP(req: Request, socketAddr?: string): string {
249
159
  return "unknown";
250
160
  }
251
161
 
252
- /** Basic IP address validation (IPv4 and IPv6) */
162
+ /**
163
+ * Basic IP address validation (IPv4 and IPv6)
164
+ */
253
165
  function isValidIP(ip: string): boolean {
254
166
  // IPv4 pattern
255
167
  const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
@@ -271,7 +183,14 @@ function isValidIP(ip: string): boolean {
271
183
  return false;
272
184
  }
273
185
 
274
- /** Parse duration string to milliseconds (supports: "100ms", "10s", "5m", "1h", "1d") */
186
+ // ==========================================
187
+ // Rate Limit Helper Utilities
188
+ // ==========================================
189
+
190
+ /**
191
+ * Parse duration string to milliseconds
192
+ * Supports: "100ms", "10s", "5m", "1h", "1d"
193
+ */
275
194
  export function parseDuration(duration: string): number {
276
195
  const match = duration.match(/^(\d+)(ms|s|m|h|d)$/);
277
196
  if (!match || !match[1] || !match[2]) {
@@ -291,7 +210,9 @@ export function parseDuration(duration: string): number {
291
210
  }
292
211
  }
293
212
 
294
- /** Create a rate limit key for a specific route + IP combination */
213
+ /**
214
+ * Create a rate limit key for a specific route + IP combination
215
+ */
295
216
  export function createRateLimitKey(route: string, ip: string): string {
296
217
  return `ratelimit:${route}:${ip}`;
297
218
  }
package/src/core/sse.ts CHANGED
@@ -1,16 +1,5 @@
1
1
  // Core SSE Service
2
- // Server-Sent Events for server→client push with channel validation
3
-
4
- import { z } from "zod";
5
-
6
- export interface SSEChannelConfig {
7
- /** Event schemas for this channel */
8
- events?: Record<string, z.ZodType<any>>;
9
- /** If true, channel name is a pattern (e.g., "user:*") */
10
- pattern?: boolean;
11
- /** Optional description */
12
- description?: string;
13
- }
2
+ // Server-Sent Events for server→client push
14
3
 
15
4
  export interface SSEClient {
16
5
  id: string;
@@ -26,27 +15,14 @@ export interface SSEConfig {
26
15
  }
27
16
 
28
17
  export interface SSE {
29
- // Channel registration (strict - channels must be registered)
30
- registerChannel(name: string, config?: SSEChannelConfig): void;
31
- registerChannels(channels: Record<string, SSEChannelConfig>): void;
32
- isChannelRegistered(channel: string): boolean;
33
- getChannelConfig(channel: string): SSEChannelConfig | undefined;
34
- /** List all registered channel names (including patterns) */
35
- listChannels(): string[];
36
-
37
- // Client management
38
18
  addClient(options?: { lastEventId?: string }): { client: SSEClient; response: Response };
39
19
  removeClient(clientId: string): void;
40
20
  getClient(clientId: string): SSEClient | undefined;
41
21
  subscribe(clientId: string, channel: string): boolean;
42
22
  unsubscribe(clientId: string, channel: string): boolean;
43
-
44
- // Broadcasting (validates against registered channel schemas)
45
23
  broadcast(channel: string, event: string, data: any, id?: string): void;
46
24
  broadcastAll(event: string, data: any, id?: string): void;
47
25
  sendTo(clientId: string, event: string, data: any, id?: string): boolean;
48
-
49
- // Queries
50
26
  getClients(): SSEClient[];
51
27
  getClientsByChannel(channel: string): SSEClient[];
52
28
  shutdown(): void;
@@ -54,8 +30,6 @@ export interface SSE {
54
30
 
55
31
  class SSEImpl implements SSE {
56
32
  private clients = new Map<string, SSEClient>();
57
- private channels = new Map<string, SSEChannelConfig>();
58
- private channelPatterns: Array<{ pattern: string; regex: RegExp; config: SSEChannelConfig }> = [];
59
33
  private heartbeatInterval: number;
60
34
  private retryInterval: number;
61
35
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
@@ -72,48 +46,6 @@ class SSEImpl implements SSE {
72
46
  }, this.heartbeatInterval);
73
47
  }
74
48
 
75
- registerChannel(name: string, config: SSEChannelConfig = {}): void {
76
- if (config.pattern || name.includes("*")) {
77
- const regex = new RegExp(
78
- "^" + name.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
79
- );
80
- this.channelPatterns.push({ pattern: name, regex, config });
81
- } else {
82
- this.channels.set(name, config);
83
- }
84
- }
85
-
86
- registerChannels(channels: Record<string, SSEChannelConfig>): void {
87
- for (const [name, config] of Object.entries(channels)) {
88
- this.registerChannel(name, config);
89
- }
90
- }
91
-
92
- isChannelRegistered(channel: string): boolean {
93
- if (this.channels.has(channel)) return true;
94
- return this.channelPatterns.some(({ regex }) => regex.test(channel));
95
- }
96
-
97
- getChannelConfig(channel: string): SSEChannelConfig | undefined {
98
- // Direct match first
99
- const direct = this.channels.get(channel);
100
- if (direct) return direct;
101
-
102
- // Pattern match (most specific first - by pattern length)
103
- const matches = this.channelPatterns
104
- .filter(({ regex }) => regex.test(channel))
105
- .sort((a, b) => b.pattern.length - a.pattern.length);
106
-
107
- return matches[0]?.config;
108
- }
109
-
110
- listChannels(): string[] {
111
- return [
112
- ...Array.from(this.channels.keys()),
113
- ...this.channelPatterns.map(p => p.pattern),
114
- ];
115
- }
116
-
117
49
  addClient(options: { lastEventId?: string } = {}): { client: SSEClient; response: Response } {
118
50
  const id = `sse_${++this.clientCounter}_${Date.now()}`;
119
51
 
@@ -184,21 +116,6 @@ class SSEImpl implements SSE {
184
116
  }
185
117
 
186
118
  broadcast(channel: string, event: string, data: any, id?: string): void {
187
- // Strict mode: channel must be registered
188
- const config = this.getChannelConfig(channel);
189
- if (!config) {
190
- throw new Error(`SSE channel '${channel}' is not registered. Register it first with sse.registerChannel() or in plugin sseChannels config.`);
191
- }
192
-
193
- // Validate event data if schema exists
194
- if (config.events?.[event]) {
195
- const schema = config.events[event];
196
- const result = schema.safeParse(data);
197
- if (!result.success) {
198
- throw new Error(`SSE event '${event}' on channel '${channel}' validation failed: ${result.error.message}`);
199
- }
200
- }
201
-
202
119
  for (const client of this.clients.values()) {
203
120
  if (client.channels.has(channel)) {
204
121
  this.sendEvent(client, event, data, id);
package/src/core.ts CHANGED
@@ -3,12 +3,11 @@ import { readdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import type { z } from "zod";
5
5
  import type { Logger } from "./core/logger";
6
- export type { Logger } from "./core/logger";
7
- import type { Cache, NamespacedCache } from "./core/cache";
6
+ import type { Cache } from "./core/cache";
8
7
  import type { Events } from "./core/events";
9
- import type { Cron, CronTaskDefinition } from "./core/cron";
10
- import type { Jobs, JobDefinition } from "./core/jobs";
11
- import type { SSE, SSEChannelConfig } from "./core/sse";
8
+ import type { Cron } from "./core/cron";
9
+ import type { Jobs } from "./core/jobs";
10
+ import type { SSE } from "./core/sse";
12
11
  import type { RateLimiter } from "./core/rate-limiter";
13
12
  import type { Errors, CustomErrorRegistry } from "./core/errors";
14
13
 
@@ -81,39 +80,15 @@ export interface GlobalContext {
81
80
  }
82
81
 
83
82
  export class PluginContext<Deps = any, Schema = any, Config = void> {
84
- private _logger?: Logger;
85
- private _cache?: NamespacedCache;
86
-
87
83
  constructor(
88
84
  public readonly core: CoreServices,
89
85
  public readonly deps: Deps,
90
- public readonly config: Config,
91
- public readonly pluginName?: string
86
+ public readonly config: Config
92
87
  ) {}
93
88
 
94
89
  get db(): Kysely<Schema> {
95
90
  return this.core.db as unknown as Kysely<Schema>;
96
91
  }
97
-
98
- /** Get a child logger with plugin context */
99
- get logger(): Logger {
100
- if (!this._logger) {
101
- this._logger = this.pluginName
102
- ? this.core.logger.child({ plugin: this.pluginName })
103
- : this.core.logger;
104
- }
105
- return this._logger;
106
- }
107
-
108
- /** Get a namespaced cache for this plugin */
109
- get cache(): NamespacedCache {
110
- if (!this._cache) {
111
- this._cache = this.pluginName
112
- ? this.core.cache.namespace(this.pluginName)
113
- : this.core.cache.namespace("default");
114
- }
115
- return this._cache;
116
- }
117
92
  }
118
93
 
119
94
  type UnionToIntersection<U> =
@@ -221,12 +196,6 @@ export class PluginBuilder<LocalSchema = {}> {
221
196
  service: Service
222
197
  ) => Middleware;
223
198
  events?: Events;
224
- /** SSE channel configurations with event schemas */
225
- sseChannels?: Record<string, SSEChannelConfig>;
226
- /** Job definitions with schemas for payload validation */
227
- jobs?: Record<string, JobDefinition>;
228
- /** Cron task definitions */
229
- cronTasks?: Record<string, CronTaskDefinition>;
230
199
  client?: ClientConfig;
231
200
  customErrors?: CustomErrors;
232
201
  service: (
@@ -250,8 +219,6 @@ export class PluginBuilder<LocalSchema = {}> {
250
219
  handlers?: Handlers;
251
220
  middleware?: (ctx: PluginContext<ExtractServices<Deps>, LocalSchema & ExtractSchemas<Deps>, void>, service: Service) => Middleware;
252
221
  events?: Events;
253
- jobs?: Record<string, JobDefinition>;
254
- cronTasks?: Record<string, CronTaskDefinition>;
255
222
  client?: ClientConfig;
256
223
  customErrors?: CustomErrors;
257
224
  } {
@@ -284,12 +251,6 @@ export class ConfiguredPluginBuilder<LocalSchema, Config> {
284
251
  service: Service
285
252
  ) => Middleware;
286
253
  events?: Events;
287
- /** SSE channel configurations with event schemas */
288
- sseChannels?: Record<string, SSEChannelConfig>;
289
- /** Job definitions with schemas for payload validation */
290
- jobs?: Record<string, JobDefinition>;
291
- /** Cron task definitions */
292
- cronTasks?: Record<string, CronTaskDefinition>;
293
254
  client?: ClientConfig;
294
255
  customErrors?: CustomErrors;
295
256
  service: (
@@ -314,9 +275,6 @@ export class ConfiguredPluginBuilder<LocalSchema, Config> {
314
275
  handlers?: Handlers;
315
276
  middleware?: (ctx: PluginContext<ExtractServices<Deps>, LocalSchema & ExtractSchemas<Deps>, Config>, service: Service) => Middleware;
316
277
  events?: Events;
317
- sseChannels?: Record<string, SSEChannelConfig>;
318
- jobs?: Record<string, JobDefinition>;
319
- cronTasks?: Record<string, CronTaskDefinition>;
320
278
  client?: ClientConfig;
321
279
  customErrors?: CustomErrors;
322
280
  }> {
@@ -346,12 +304,6 @@ export type InferCustomErrors<T> = UnwrapPluginFactory<T> extends { customErrors
346
304
 
347
305
  export type { ExtractServices, ExtractSchemas };
348
306
 
349
- export interface Migration {
350
- name: string;
351
- up: (db: Kysely<any>) => Promise<void>;
352
- down?: (db: Kysely<any>) => Promise<void>;
353
- }
354
-
355
307
  export type Plugin = {
356
308
  name: string;
357
309
  version?: string;
@@ -360,14 +312,6 @@ export type Plugin = {
360
312
  /** Middleware function - receives PluginContext and service, returns middleware definitions */
361
313
  middleware?: (ctx: any, service: any) => Record<string, any>;
362
314
  events?: Record<string, any>;
363
- /** SSE channel configurations with event schemas */
364
- sseChannels?: Record<string, SSEChannelConfig>;
365
- /** Job definitions with schemas for payload validation */
366
- jobs?: Record<string, JobDefinition>;
367
- /** Cron task definitions */
368
- cronTasks?: Record<string, CronTaskDefinition>;
369
- /** Inline migrations (alternative to file-based migrations) */
370
- migrations?: Migration[];
371
315
  client?: ClientConfig;
372
316
  customErrors?: CustomErrorRegistry;
373
317
  service: (ctx: any) => any;
@@ -410,28 +354,13 @@ export class PluginManager {
410
354
  }
411
355
 
412
356
  async migrate(): Promise<void> {
357
+ console.log("Running migrations (File-System Based)...");
413
358
  const sortedPlugins = this.resolveOrder();
414
359
 
415
360
  for (const plugin of sortedPlugins) {
416
361
  const pluginName = plugin.name;
417
-
418
- // 1. Check for inline migrations (migrations array in plugin definition)
419
- if (plugin.migrations && Array.isArray(plugin.migrations) && plugin.migrations.length > 0) {
420
- for (const migration of plugin.migrations) {
421
- if (migration.up) {
422
- try {
423
- await migration.up(this.core.db);
424
- } catch (e) {
425
- console.error(`[${pluginName}] Migration '${migration.name}' failed:`, e);
426
- }
427
- }
428
- }
429
- continue; // Skip file-based migrations if inline migrations exist
430
- }
431
-
432
- // 2. Fall back to file-based migrations
433
362
  const possibleMigrationDirs = [
434
- join(process.cwd(), "examples/starter/src/plugins", pluginName, "migrations"),
363
+ join(process.cwd(), "examples/basic-server/src/plugins", pluginName, "migrations"),
435
364
  join(process.cwd(), "src/plugins", pluginName, "migrations"),
436
365
  join(process.cwd(), "plugins", pluginName, "migrations"),
437
366
  ];
@@ -454,15 +383,19 @@ export class PluginManager {
454
383
  const migrationFiles = files.filter(f => f.endsWith(".ts"));
455
384
 
456
385
  if (migrationFiles.length > 0) {
386
+ console.log(`[Migration] checking plugin: ${pluginName} at ${migrationDir}`);
387
+
457
388
  for (const file of migrationFiles.sort()) {
389
+ console.log(` - Executing migration: ${file}`);
458
390
  const migrationPath = join(migrationDir, file);
459
391
  const migration = await import(migrationPath);
460
392
 
461
393
  if (migration.up) {
462
394
  try {
463
395
  await migration.up(this.core.db);
396
+ console.log(` Success`);
464
397
  } catch (e) {
465
- console.error(`[${pluginName}] Migration '${file}' failed:`, e);
398
+ console.error(` Failed to run ${file}:`, e);
466
399
  }
467
400
  }
468
401
  }
@@ -495,30 +428,6 @@ export class PluginManager {
495
428
  }
496
429
  }
497
430
 
498
- // Auto-register plugin event schemas
499
- if (plugin.events) {
500
- this.core.events.registerMany(plugin.events as Record<string, any>);
501
- console.log(`[${plugin.name}] Registered ${Object.keys(plugin.events).length} event schemas`);
502
- }
503
-
504
- // Auto-register plugin SSE channels
505
- if (plugin.sseChannels) {
506
- this.core.sse.registerChannels(plugin.sseChannels);
507
- console.log(`[${plugin.name}] Registered ${Object.keys(plugin.sseChannels).length} SSE channels`);
508
- }
509
-
510
- // Auto-register plugin jobs (with schema validation)
511
- if (plugin.jobs) {
512
- this.core.jobs.registerJobs(plugin.jobs, plugin.name);
513
- console.log(`[${plugin.name}] Registered ${Object.keys(plugin.jobs).length} job handlers`);
514
- }
515
-
516
- // Auto-register plugin cron tasks
517
- if (plugin.cronTasks) {
518
- this.core.cron.registerTasks(plugin.cronTasks, plugin.name);
519
- console.log(`[${plugin.name}] Registered ${Object.keys(plugin.cronTasks).length} cron tasks`);
520
- }
521
-
522
431
  const pluginDeps: Record<string, unknown> = {};
523
432
  if (plugin.dependencies) {
524
433
  for (const depName of plugin.dependencies) {
@@ -527,7 +436,7 @@ export class PluginManager {
527
436
  }
528
437
 
529
438
  const pluginConfig = (plugin as ConfiguredPlugin)._boundConfig;
530
- const ctx = new PluginContext(this.core, pluginDeps, pluginConfig, plugin.name);
439
+ const ctx = new PluginContext(this.core, pluginDeps, pluginConfig);
531
440
  const service = await plugin.service(ctx);
532
441
 
533
442
  if (service) {