@typokit/plugin-debug 0.1.4

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,35 @@
1
+ import type { TypoKitPlugin } from "@typokit/core";
2
+ /** Security configuration for production mode */
3
+ export interface DebugSecurityConfig {
4
+ /** API key required via X-Debug-Key header */
5
+ apiKey?: string;
6
+ /** IP/CIDR allowlist (e.g., ["127.0.0.1", "10.0.0.0/8"]) */
7
+ allowlist?: string[];
8
+ /** Hostname to bind to (default: "127.0.0.1" in production) */
9
+ hostname?: string;
10
+ /** Field paths to redact from responses (e.g., ["*.password", "authorization"]) */
11
+ redact?: string[];
12
+ /** Rate limit: max requests per window */
13
+ rateLimit?: number;
14
+ /** Rate limit window in milliseconds (default: 60000) */
15
+ rateLimitWindow?: number;
16
+ }
17
+ /** Options for the debugPlugin factory */
18
+ export interface DebugPluginOptions {
19
+ /** Port for the debug sidecar (default: 9800) */
20
+ port?: number;
21
+ /** Enable in production mode (default: false — only auto-enabled in dev) */
22
+ production?: boolean;
23
+ /** Security config (required in production mode) */
24
+ security?: DebugSecurityConfig;
25
+ }
26
+ /**
27
+ * Create a debug sidecar plugin that exposes introspection endpoints
28
+ * on a separate port.
29
+ *
30
+ * Development mode (default): no auth required, binds to 0.0.0.0.
31
+ * Production mode (opt-in): requires apiKey, supports IP allowlist,
32
+ * binds to 127.0.0.1 by default.
33
+ */
34
+ export declare function debugPlugin(options?: DebugPluginOptions): TypoKitPlugin;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,eAAe,CAAC;AAkBhE,iDAAiD;AACjD,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CAChC;AAgHD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,aAAa,CAoW3E"}
package/dist/index.js ADDED
@@ -0,0 +1,382 @@
1
+ // @typokit/plugin-debug — Debug Sidecar Server
2
+ //
3
+ // A plugin that runs a read-only debug HTTP server on a separate port,
4
+ // exposing structured introspection endpoints for AI agents and dev tools.
5
+ import { redactFields } from "@typokit/otel";
6
+ import { createServer } from "@typokit/platform-node";
7
+ // ─── Route Table Traversal ───────────────────────────────────
8
+ function collectRoutes(node, pathPrefix) {
9
+ const routes = [];
10
+ const currentPath = pathPrefix + (node.segment ? `/${node.segment}` : "");
11
+ if (node.handlers) {
12
+ for (const [method, handler] of Object.entries(node.handlers)) {
13
+ if (handler) {
14
+ routes.push({
15
+ method: method,
16
+ path: currentPath || "/",
17
+ ref: handler.ref,
18
+ middleware: handler.middleware,
19
+ ...(handler.validators ? { validators: handler.validators } : {}),
20
+ ...(handler.serializer ? { serializer: handler.serializer } : {}),
21
+ });
22
+ }
23
+ }
24
+ }
25
+ if (node.children) {
26
+ for (const child of Object.values(node.children)) {
27
+ routes.push(...collectRoutes(child, currentPath));
28
+ }
29
+ }
30
+ if (node.paramChild) {
31
+ const paramPath = `${currentPath}/:${node.paramChild.paramName}`;
32
+ routes.push(...collectRoutes(node.paramChild, paramPath.replace(`/${node.paramChild.segment}`, "")));
33
+ }
34
+ if (node.wildcardChild) {
35
+ const wcPath = `${currentPath}/*${node.wildcardChild.paramName}`;
36
+ routes.push(...collectRoutes(node.wildcardChild, wcPath.replace(`/${node.wildcardChild.segment}`, "")));
37
+ }
38
+ return routes;
39
+ }
40
+ // ─── CIDR Check ──────────────────────────────────────────────
41
+ function ipToLong(ip) {
42
+ const parts = ip.split(".").map(Number);
43
+ return (((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0);
44
+ }
45
+ function isIpAllowed(clientIp, allowlist) {
46
+ if (allowlist.length === 0)
47
+ return true;
48
+ const clientLong = ipToLong(clientIp);
49
+ for (const entry of allowlist) {
50
+ if (entry.includes("/")) {
51
+ const [network, bits] = entry.split("/");
52
+ const mask = (~0 << (32 - Number(bits))) >>> 0;
53
+ if ((clientLong & mask) === (ipToLong(network) & mask))
54
+ return true;
55
+ }
56
+ else {
57
+ if (clientIp === entry)
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ // ─── Percentile Calculation ──────────────────────────────────
64
+ function percentile(sorted, p) {
65
+ if (sorted.length === 0)
66
+ return 0;
67
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
68
+ return sorted[Math.max(0, idx)];
69
+ }
70
+ // ─── Debug Plugin Factory ────────────────────────────────────
71
+ /**
72
+ * Create a debug sidecar plugin that exposes introspection endpoints
73
+ * on a separate port.
74
+ *
75
+ * Development mode (default): no auth required, binds to 0.0.0.0.
76
+ * Production mode (opt-in): requires apiKey, supports IP allowlist,
77
+ * binds to 127.0.0.1 by default.
78
+ */
79
+ export function debugPlugin(options = {}) {
80
+ const port = options.port ?? 9800;
81
+ const isProduction = options.production ?? false;
82
+ const security = options.security ?? {};
83
+ const redactPatterns = security.redact ?? [];
84
+ const rateLimit = security.rateLimit ?? 0;
85
+ const rateLimitWindow = security.rateLimitWindow ?? 60_000;
86
+ // Internal state
87
+ let cachedRoutes = [];
88
+ let middlewareNames = [];
89
+ const recentErrors = [];
90
+ const recentTraces = [];
91
+ const recentLogs = [];
92
+ const performanceData = [];
93
+ let serverHandle = null;
94
+ let dependencies = {};
95
+ // Rate limiting state
96
+ const rateLimitMap = new Map();
97
+ function checkRateLimit(clientIp) {
98
+ if (rateLimit <= 0)
99
+ return true;
100
+ const now = Date.now();
101
+ const entry = rateLimitMap.get(clientIp);
102
+ if (!entry || now >= entry.resetAt) {
103
+ rateLimitMap.set(clientIp, { count: 1, resetAt: now + rateLimitWindow });
104
+ return true;
105
+ }
106
+ entry.count++;
107
+ return entry.count <= rateLimit;
108
+ }
109
+ function getClientIp(req) {
110
+ const forwarded = req.headers["x-forwarded-for"];
111
+ if (typeof forwarded === "string")
112
+ return forwarded.split(",")[0].trim();
113
+ return "127.0.0.1";
114
+ }
115
+ // Security middleware
116
+ function checkSecurity(req) {
117
+ if (!isProduction)
118
+ return null;
119
+ // API key check
120
+ if (security.apiKey) {
121
+ const key = req.headers["x-debug-key"];
122
+ if (key !== security.apiKey) {
123
+ return {
124
+ status: 401,
125
+ headers: { "content-type": "application/json" },
126
+ body: {
127
+ error: {
128
+ code: "UNAUTHORIZED",
129
+ message: "Invalid or missing X-Debug-Key header",
130
+ },
131
+ },
132
+ };
133
+ }
134
+ }
135
+ // IP allowlist check
136
+ if (security.allowlist && security.allowlist.length > 0) {
137
+ const clientIp = getClientIp(req);
138
+ if (!isIpAllowed(clientIp, security.allowlist)) {
139
+ return {
140
+ status: 403,
141
+ headers: { "content-type": "application/json" },
142
+ body: { error: { code: "FORBIDDEN", message: "IP not allowed" } },
143
+ };
144
+ }
145
+ }
146
+ // Rate limiting
147
+ if (rateLimit > 0) {
148
+ const clientIp = getClientIp(req);
149
+ if (!checkRateLimit(clientIp)) {
150
+ return {
151
+ status: 429,
152
+ headers: { "content-type": "application/json" },
153
+ body: {
154
+ error: { code: "RATE_LIMITED", message: "Too many requests" },
155
+ },
156
+ };
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ function maybeRedact(data) {
162
+ if (redactPatterns.length === 0)
163
+ return data;
164
+ return redactFields(data, redactPatterns);
165
+ }
166
+ function parseDuration(value) {
167
+ if (!value || Array.isArray(value))
168
+ return 300_000; // default 5 min
169
+ const match = value.match(/^(\d+)(ms|s|m|h)?$/);
170
+ if (!match)
171
+ return 300_000;
172
+ const num = Number(match[1]);
173
+ switch (match[2]) {
174
+ case "ms":
175
+ return num;
176
+ case "s":
177
+ return num * 1000;
178
+ case "m":
179
+ return num * 60_000;
180
+ case "h":
181
+ return num * 3_600_000;
182
+ default:
183
+ return num * 1000; // default to seconds
184
+ }
185
+ }
186
+ // Endpoint handlers
187
+ const endpoints = {
188
+ "/_debug/routes": () => {
189
+ return { routes: cachedRoutes };
190
+ },
191
+ "/_debug/middleware": () => {
192
+ return { middleware: middlewareNames };
193
+ },
194
+ "/_debug/performance": (req) => {
195
+ const windowMs = parseDuration(req.query["window"]);
196
+ const cutoff = new Date(Date.now() - windowMs).toISOString();
197
+ const relevant = performanceData.filter((d) => d.timestamp >= cutoff);
198
+ const durations = relevant.map((d) => d.value).sort((a, b) => a - b);
199
+ return {
200
+ window: `${windowMs}ms`,
201
+ count: durations.length,
202
+ p50: percentile(durations, 50),
203
+ p95: percentile(durations, 95),
204
+ p99: percentile(durations, 99),
205
+ min: durations.length > 0 ? durations[0] : 0,
206
+ max: durations.length > 0 ? durations[durations.length - 1] : 0,
207
+ };
208
+ },
209
+ "/_debug/errors": (req) => {
210
+ const sinceMs = parseDuration(req.query["since"]);
211
+ const cutoff = new Date(Date.now() - sinceMs).toISOString();
212
+ const filtered = recentErrors.filter((e) => e.timestamp >= cutoff);
213
+ return {
214
+ errors: filtered.map((e) => redactPatterns.length > 0
215
+ ? {
216
+ ...e,
217
+ ...(e.details ? { details: maybeRedact(e.details) } : {}),
218
+ }
219
+ : e),
220
+ };
221
+ },
222
+ "/_debug/health": () => {
223
+ const proc = globalThis.process;
224
+ const mem = proc?.memoryUsage?.();
225
+ return {
226
+ status: "ok",
227
+ uptime: Date.now(),
228
+ memory: mem
229
+ ? { heapUsed: mem.heapUsed, heapTotal: mem.heapTotal, rss: mem.rss }
230
+ : null,
231
+ };
232
+ },
233
+ "/_debug/dependencies": () => {
234
+ return { dependencies };
235
+ },
236
+ "/_debug/traces": () => {
237
+ return {
238
+ traces: recentTraces.slice(-100).map((spans) => spans.map((s) => redactPatterns.length > 0
239
+ ? {
240
+ ...s,
241
+ attributes: maybeRedact(s.attributes),
242
+ }
243
+ : s)),
244
+ };
245
+ },
246
+ "/_debug/logs": (req) => {
247
+ const sinceMs = parseDuration(req.query["since"]);
248
+ const cutoff = new Date(Date.now() - sinceMs).toISOString();
249
+ const filtered = recentLogs.filter((l) => l.timestamp >= cutoff);
250
+ return {
251
+ logs: filtered.map((l) => redactPatterns.length > 0 && l.data
252
+ ? { ...l, data: maybeRedact(l.data) }
253
+ : l),
254
+ };
255
+ },
256
+ };
257
+ async function handleRequest(req) {
258
+ // Only GET requests allowed (read-only)
259
+ if (req.method !== "GET") {
260
+ return {
261
+ status: 405,
262
+ headers: { "content-type": "application/json", allow: "GET" },
263
+ body: {
264
+ error: {
265
+ code: "METHOD_NOT_ALLOWED",
266
+ message: "Debug endpoints are read-only",
267
+ },
268
+ },
269
+ };
270
+ }
271
+ // Security check
272
+ const secError = checkSecurity(req);
273
+ if (secError)
274
+ return secError;
275
+ const handler = endpoints[req.path];
276
+ if (!handler) {
277
+ return {
278
+ status: 404,
279
+ headers: { "content-type": "application/json" },
280
+ body: {
281
+ error: {
282
+ code: "NOT_FOUND",
283
+ message: `Unknown debug endpoint: ${req.path}`,
284
+ },
285
+ },
286
+ };
287
+ }
288
+ const body = handler(req);
289
+ return {
290
+ status: 200,
291
+ headers: { "content-type": "application/json" },
292
+ body,
293
+ };
294
+ }
295
+ const plugin = {
296
+ name: "plugin-debug",
297
+ async onStart(app) {
298
+ // Collect middleware names from plugins
299
+ middlewareNames = app.plugins
300
+ .filter((p) => p.name !== "plugin-debug")
301
+ .map((p) => p.name);
302
+ // Build dependency graph from services
303
+ dependencies = {};
304
+ for (const [key, value] of Object.entries(app.services)) {
305
+ if (typeof value === "object" &&
306
+ value !== null &&
307
+ "dependencies" in value) {
308
+ dependencies[key] = value.dependencies;
309
+ }
310
+ }
311
+ // Expose data collection APIs via services
312
+ app.services["_debug"] = {
313
+ recordError: (error, route) => {
314
+ recentErrors.push({
315
+ timestamp: new Date().toISOString(),
316
+ code: error.code,
317
+ status: error.status,
318
+ message: error.message,
319
+ details: error.details,
320
+ route,
321
+ });
322
+ // Keep at most 1000 errors
323
+ if (recentErrors.length > 1000)
324
+ recentErrors.splice(0, recentErrors.length - 1000);
325
+ },
326
+ recordTrace: (spans) => {
327
+ recentTraces.push(spans);
328
+ if (recentTraces.length > 500)
329
+ recentTraces.splice(0, recentTraces.length - 500);
330
+ },
331
+ recordLog: (entry) => {
332
+ recentLogs.push(entry);
333
+ if (recentLogs.length > 2000)
334
+ recentLogs.splice(0, recentLogs.length - 2000);
335
+ },
336
+ recordPerformance: (dataPoint) => {
337
+ performanceData.push(dataPoint);
338
+ if (performanceData.length > 5000)
339
+ performanceData.splice(0, performanceData.length - 5000);
340
+ },
341
+ setRouteTable: (routeTable) => {
342
+ cachedRoutes = collectRoutes(routeTable, "");
343
+ },
344
+ setMiddleware: (names) => {
345
+ middlewareNames = names;
346
+ },
347
+ };
348
+ },
349
+ async onReady(_app) {
350
+ const hostname = isProduction
351
+ ? (security.hostname ?? "127.0.0.1")
352
+ : "0.0.0.0";
353
+ const srv = createServer(handleRequest, { hostname });
354
+ serverHandle = await srv.listen(port);
355
+ },
356
+ onError(error, ctx) {
357
+ recentErrors.push({
358
+ timestamp: new Date().toISOString(),
359
+ code: error.code,
360
+ status: error.status,
361
+ message: error.message,
362
+ details: error.details,
363
+ route: ctx.requestId,
364
+ });
365
+ if (recentErrors.length > 1000)
366
+ recentErrors.splice(0, recentErrors.length - 1000);
367
+ },
368
+ async onStop(_app) {
369
+ if (serverHandle) {
370
+ await serverHandle.close();
371
+ serverHandle = null;
372
+ }
373
+ },
374
+ onSchemaChange(_changes) {
375
+ // Route map will be refreshed by the next build cycle calling setRouteTable
376
+ // Clear cached routes so they'll be re-populated
377
+ cachedRoutes = [];
378
+ },
379
+ };
380
+ return plugin;
381
+ }
382
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@typokit/plugin-debug",
3
+ "exports": {
4
+ ".": {
5
+ "import": "./dist/index.js",
6
+ "types": "./dist/index.d.ts"
7
+ }
8
+ },
9
+ "version": "0.1.4",
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "dependencies": {
18
+ "@typokit/core": "0.1.4",
19
+ "@typokit/types": "0.1.4",
20
+ "@typokit/errors": "0.1.4",
21
+ "@typokit/platform-node": "0.1.4",
22
+ "@typokit/otel": "0.1.4"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/KyleBastien/typokit",
30
+ "directory": "packages/plugin-debug"
31
+ },
32
+ "scripts": {
33
+ "test": "rstest run --passWithNoTests"
34
+ }
35
+ }
@@ -0,0 +1,342 @@
1
+ // @typokit/plugin-debug — Integration Tests
2
+
3
+ import { describe, it, expect } from "@rstest/core";
4
+ import { debugPlugin } from "./index.js";
5
+ import type { TypoKitPlugin, AppInstance } from "@typokit/core";
6
+ import type { CompiledRouteTable, SchemaChange } from "@typokit/types";
7
+ import type { HistogramDataPoint, LogEntry, SpanData } from "@typokit/otel";
8
+
9
+ // ─── Helpers ─────────────────────────────────────────────────
10
+
11
+ function createTestApp(plugins: TypoKitPlugin[]): AppInstance {
12
+ return {
13
+ name: "test-app",
14
+ plugins,
15
+ services: {},
16
+ };
17
+ }
18
+
19
+ const sampleRouteTable: CompiledRouteTable = {
20
+ segment: "",
21
+ children: {
22
+ users: {
23
+ segment: "users",
24
+ handlers: {
25
+ GET: { ref: "users#list", middleware: ["auth"] },
26
+ POST: { ref: "users#create", middleware: ["auth", "validate"] },
27
+ },
28
+ paramChild: {
29
+ segment: ":id",
30
+ paramName: "id",
31
+ handlers: {
32
+ GET: {
33
+ ref: "users#get",
34
+ middleware: ["auth"],
35
+ validators: { params: "userId" },
36
+ },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ };
42
+
43
+ interface FetchOptions {
44
+ method?: string;
45
+ headers?: Record<string, string>;
46
+ }
47
+
48
+ async function fetchDebug(
49
+ port: number,
50
+ path: string,
51
+ options: FetchOptions = {},
52
+ ): Promise<{ status: number; body: Record<string, unknown> }> {
53
+ const fetchFn = globalThis.fetch;
54
+ const resp = await fetchFn(`http://127.0.0.1:${port}${path}`, {
55
+ method: options.method ?? "GET",
56
+ headers: options.headers ?? {},
57
+ });
58
+ const body = (await resp.json()) as Record<string, unknown>;
59
+ return { status: resp.status, body };
60
+ }
61
+
62
+ // ─── Tests ───────────────────────────────────────────────────
63
+
64
+ describe("debugPlugin", () => {
65
+ it("should create a plugin with the correct name", () => {
66
+ const plugin = debugPlugin();
67
+ expect(plugin.name).toBe("plugin-debug");
68
+ });
69
+
70
+ it("should implement TypoKitPlugin lifecycle hooks", () => {
71
+ const plugin = debugPlugin();
72
+ expect(typeof plugin.onStart).toBe("function");
73
+ expect(typeof plugin.onReady).toBe("function");
74
+ expect(typeof plugin.onStop).toBe("function");
75
+ expect(typeof plugin.onError).toBe("function");
76
+ expect(typeof plugin.onSchemaChange).toBe("function");
77
+ });
78
+
79
+ it("should start and stop the sidecar server", async () => {
80
+ const plugin = debugPlugin({ port: 0 });
81
+ const app = createTestApp([plugin]);
82
+
83
+ await plugin.onStart!(app);
84
+ // Use a random port for tests
85
+ // onReady starts the server
86
+ await plugin.onReady!(app);
87
+
88
+ // Server should be running — stop it
89
+ await plugin.onStop!(app);
90
+ });
91
+
92
+ it("should clear cached routes on schema change", () => {
93
+ const plugin = debugPlugin();
94
+ const changes: SchemaChange[] = [
95
+ { type: "add", entity: "User", field: "email" },
96
+ ];
97
+ // Should not throw
98
+ plugin.onSchemaChange!(changes);
99
+ });
100
+ });
101
+
102
+ describe("debug endpoints", () => {
103
+ let plugin: TypoKitPlugin;
104
+ let app: AppInstance;
105
+ const port = 19800; // Use non-default port for tests
106
+
107
+ // Start the debug server before tests
108
+ it("should start the debug sidecar", async () => {
109
+ plugin = debugPlugin({ port });
110
+ app = createTestApp([plugin]);
111
+ await plugin.onStart!(app);
112
+
113
+ // Set up test data via the services API
114
+ const debug = app.services["_debug"] as {
115
+ setRouteTable: (rt: CompiledRouteTable) => void;
116
+ setMiddleware: (names: string[]) => void;
117
+ recordError: (
118
+ error: {
119
+ code: string;
120
+ status: number;
121
+ message: string;
122
+ details?: Record<string, unknown>;
123
+ },
124
+ route?: string,
125
+ ) => void;
126
+ recordTrace: (spans: SpanData[]) => void;
127
+ recordLog: (entry: LogEntry) => void;
128
+ recordPerformance: (dp: HistogramDataPoint) => void;
129
+ };
130
+
131
+ debug.setRouteTable(sampleRouteTable);
132
+ debug.setMiddleware(["auth", "cors", "logging"]);
133
+ debug.recordError(
134
+ { code: "NOT_FOUND", status: 404, message: "User not found" },
135
+ "GET /users/999",
136
+ );
137
+ debug.recordTrace([
138
+ {
139
+ traceId: "abc123",
140
+ spanId: "span1",
141
+ name: "GET /users",
142
+ kind: "server",
143
+ startTime: new Date().toISOString(),
144
+ endTime: new Date().toISOString(),
145
+ durationMs: 42,
146
+ status: "ok",
147
+ attributes: { "http.method": "GET" },
148
+ },
149
+ ]);
150
+ debug.recordLog({
151
+ level: "info",
152
+ message: "Request processed",
153
+ timestamp: new Date().toISOString(),
154
+ data: { userId: "123" },
155
+ });
156
+ debug.recordPerformance({
157
+ labels: { route: "GET /users", method: "GET", status: 200 },
158
+ value: 42,
159
+ timestamp: new Date().toISOString(),
160
+ });
161
+
162
+ await plugin.onReady!(app);
163
+ });
164
+
165
+ it("GET /_debug/routes should return registered routes", async () => {
166
+ const { status, body } = await fetchDebug(port, "/_debug/routes");
167
+ expect(status).toBe(200);
168
+ const routes = body["routes"] as Array<{ method: string; ref: string }>;
169
+ expect(Array.isArray(routes)).toBe(true);
170
+ expect(routes.length).toBeGreaterThan(0);
171
+ // Should have the users routes
172
+ const listRoute = routes.find((r) => r.ref === "users#list");
173
+ expect(listRoute).toBeDefined();
174
+ expect(listRoute!.method).toBe("GET");
175
+ });
176
+
177
+ it("GET /_debug/middleware should return middleware chain", async () => {
178
+ const { status, body } = await fetchDebug(port, "/_debug/middleware");
179
+ expect(status).toBe(200);
180
+ const mw = body["middleware"] as string[];
181
+ expect(Array.isArray(mw)).toBe(true);
182
+ expect(mw).toContain("auth");
183
+ expect(mw).toContain("cors");
184
+ });
185
+
186
+ it("GET /_debug/performance should return latency percentiles", async () => {
187
+ const { status, body } = await fetchDebug(
188
+ port,
189
+ "/_debug/performance?window=5m",
190
+ );
191
+ expect(status).toBe(200);
192
+ expect(typeof body["p50"]).toBe("number");
193
+ expect(typeof body["p95"]).toBe("number");
194
+ expect(typeof body["p99"]).toBe("number");
195
+ expect(typeof body["count"]).toBe("number");
196
+ });
197
+
198
+ it("GET /_debug/errors should return recent errors", async () => {
199
+ const { status, body } = await fetchDebug(port, "/_debug/errors?since=5m");
200
+ expect(status).toBe(200);
201
+ const errors = body["errors"] as Array<{ code: string }>;
202
+ expect(Array.isArray(errors)).toBe(true);
203
+ expect(errors.length).toBeGreaterThan(0);
204
+ expect(errors[0].code).toBe("NOT_FOUND");
205
+ });
206
+
207
+ it("GET /_debug/health should return health status", async () => {
208
+ const { status, body } = await fetchDebug(port, "/_debug/health");
209
+ expect(status).toBe(200);
210
+ expect(body["status"]).toBe("ok");
211
+ expect(body["memory"]).toBeDefined();
212
+ });
213
+
214
+ it("GET /_debug/dependencies should return dependency graph", async () => {
215
+ const { status, body } = await fetchDebug(port, "/_debug/dependencies");
216
+ expect(status).toBe(200);
217
+ expect(body["dependencies"]).toBeDefined();
218
+ });
219
+
220
+ it("GET /_debug/traces should return recent traces", async () => {
221
+ const { status, body } = await fetchDebug(port, "/_debug/traces");
222
+ expect(status).toBe(200);
223
+ const traces = body["traces"] as SpanData[][];
224
+ expect(Array.isArray(traces)).toBe(true);
225
+ expect(traces.length).toBeGreaterThan(0);
226
+ });
227
+
228
+ it("GET /_debug/logs should return recent logs", async () => {
229
+ const { status, body } = await fetchDebug(port, "/_debug/logs?since=5m");
230
+ expect(status).toBe(200);
231
+ const logs = body["logs"] as LogEntry[];
232
+ expect(Array.isArray(logs)).toBe(true);
233
+ expect(logs.length).toBeGreaterThan(0);
234
+ expect(logs[0].message).toBe("Request processed");
235
+ });
236
+
237
+ it("POST should be rejected (read-only)", async () => {
238
+ const { status } = await fetchDebug(port, "/_debug/routes", {
239
+ method: "POST",
240
+ });
241
+ expect(status).toBe(405);
242
+ });
243
+
244
+ it("unknown endpoint should return 404", async () => {
245
+ const { status } = await fetchDebug(port, "/_debug/unknown");
246
+ expect(status).toBe(404);
247
+ });
248
+
249
+ it("should stop the sidecar", async () => {
250
+ await plugin.onStop!(app);
251
+ });
252
+ });
253
+
254
+ describe("production mode security", () => {
255
+ it("should require API key in production mode", async () => {
256
+ const testPort = 19801;
257
+ const plugin = debugPlugin({
258
+ port: testPort,
259
+ production: true,
260
+ security: { apiKey: "test-secret-key" },
261
+ });
262
+ const app = createTestApp([plugin]);
263
+ await plugin.onStart!(app);
264
+ await plugin.onReady!(app);
265
+
266
+ // Request without key should fail
267
+ const { status: noKeyStatus } = await fetchDebug(
268
+ testPort,
269
+ "/_debug/health",
270
+ );
271
+ expect(noKeyStatus).toBe(401);
272
+
273
+ // Request with correct key should succeed
274
+ const { status: withKeyStatus } = await fetchDebug(
275
+ testPort,
276
+ "/_debug/health",
277
+ {
278
+ headers: { "x-debug-key": "test-secret-key" },
279
+ },
280
+ );
281
+ expect(withKeyStatus).toBe(200);
282
+
283
+ // Request with wrong key should fail
284
+ const { status: wrongKeyStatus } = await fetchDebug(
285
+ testPort,
286
+ "/_debug/health",
287
+ {
288
+ headers: { "x-debug-key": "wrong-key" },
289
+ },
290
+ );
291
+ expect(wrongKeyStatus).toBe(401);
292
+
293
+ await plugin.onStop!(app);
294
+ });
295
+ });
296
+
297
+ describe("redaction", () => {
298
+ it("should redact sensitive fields from error details", async () => {
299
+ const testPort = 19802;
300
+ const plugin = debugPlugin({
301
+ port: testPort,
302
+ security: { redact: ["password", "*.secret"] },
303
+ });
304
+ const app = createTestApp([plugin]);
305
+ await plugin.onStart!(app);
306
+
307
+ const debug = app.services["_debug"] as {
308
+ recordError: (
309
+ error: {
310
+ code: string;
311
+ status: number;
312
+ message: string;
313
+ details?: Record<string, unknown>;
314
+ },
315
+ route?: string,
316
+ ) => void;
317
+ };
318
+
319
+ debug.recordError({
320
+ code: "AUTH_FAILED",
321
+ status: 401,
322
+ message: "Auth failed",
323
+ details: { password: "hunter2", username: "admin" },
324
+ });
325
+
326
+ await plugin.onReady!(app);
327
+
328
+ const { status, body } = await fetchDebug(
329
+ testPort,
330
+ "/_debug/errors?since=5m",
331
+ );
332
+ expect(status).toBe(200);
333
+ const errors = body["errors"] as Array<{
334
+ details?: Record<string, unknown>;
335
+ }>;
336
+ expect(errors.length).toBeGreaterThan(0);
337
+ expect(errors[0].details?.["password"]).toBe("[REDACTED]");
338
+ expect(errors[0].details?.["username"]).toBe("admin");
339
+
340
+ await plugin.onStop!(app);
341
+ });
342
+ });
package/src/index.ts ADDED
@@ -0,0 +1,524 @@
1
+ // @typokit/plugin-debug — Debug Sidecar Server
2
+ //
3
+ // A plugin that runs a read-only debug HTTP server on a separate port,
4
+ // exposing structured introspection endpoints for AI agents and dev tools.
5
+
6
+ import type { TypoKitPlugin, AppInstance } from "@typokit/core";
7
+ import type {
8
+ CompiledRoute,
9
+ CompiledRouteTable,
10
+ HttpMethod,
11
+ SchemaChange,
12
+ ServerHandle,
13
+ TypoKitRequest,
14
+ TypoKitResponse,
15
+ } from "@typokit/types";
16
+ import type { AppError } from "@typokit/errors";
17
+ import type { RequestContext } from "@typokit/types";
18
+ import type { HistogramDataPoint, LogEntry, SpanData } from "@typokit/otel";
19
+ import { redactFields } from "@typokit/otel";
20
+ import { createServer } from "@typokit/platform-node";
21
+
22
+ // ─── Types ───────────────────────────────────────────────────
23
+
24
+ /** Security configuration for production mode */
25
+ export interface DebugSecurityConfig {
26
+ /** API key required via X-Debug-Key header */
27
+ apiKey?: string;
28
+ /** IP/CIDR allowlist (e.g., ["127.0.0.1", "10.0.0.0/8"]) */
29
+ allowlist?: string[];
30
+ /** Hostname to bind to (default: "127.0.0.1" in production) */
31
+ hostname?: string;
32
+ /** Field paths to redact from responses (e.g., ["*.password", "authorization"]) */
33
+ redact?: string[];
34
+ /** Rate limit: max requests per window */
35
+ rateLimit?: number;
36
+ /** Rate limit window in milliseconds (default: 60000) */
37
+ rateLimitWindow?: number;
38
+ }
39
+
40
+ /** Options for the debugPlugin factory */
41
+ export interface DebugPluginOptions {
42
+ /** Port for the debug sidecar (default: 9800) */
43
+ port?: number;
44
+ /** Enable in production mode (default: false — only auto-enabled in dev) */
45
+ production?: boolean;
46
+ /** Security config (required in production mode) */
47
+ security?: DebugSecurityConfig;
48
+ }
49
+
50
+ // ─── Internal State ──────────────────────────────────────────
51
+
52
+ interface RouteInfo {
53
+ method: HttpMethod;
54
+ path: string;
55
+ ref: string;
56
+ middleware: string[];
57
+ validators?: { params?: string; query?: string; body?: string };
58
+ serializer?: string;
59
+ }
60
+
61
+ interface ErrorRecord {
62
+ timestamp: string;
63
+ code: string;
64
+ status: number;
65
+ message: string;
66
+ details?: Record<string, unknown>;
67
+ route?: string;
68
+ }
69
+
70
+ interface RateLimitEntry {
71
+ count: number;
72
+ resetAt: number;
73
+ }
74
+
75
+ // ─── Route Table Traversal ───────────────────────────────────
76
+
77
+ function collectRoutes(node: CompiledRoute, pathPrefix: string): RouteInfo[] {
78
+ const routes: RouteInfo[] = [];
79
+ const currentPath = pathPrefix + (node.segment ? `/${node.segment}` : "");
80
+
81
+ if (node.handlers) {
82
+ for (const [method, handler] of Object.entries(node.handlers)) {
83
+ if (handler) {
84
+ routes.push({
85
+ method: method as HttpMethod,
86
+ path: currentPath || "/",
87
+ ref: handler.ref,
88
+ middleware: handler.middleware,
89
+ ...(handler.validators ? { validators: handler.validators } : {}),
90
+ ...(handler.serializer ? { serializer: handler.serializer } : {}),
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ if (node.children) {
97
+ for (const child of Object.values(node.children)) {
98
+ routes.push(...collectRoutes(child, currentPath));
99
+ }
100
+ }
101
+
102
+ if (node.paramChild) {
103
+ const paramPath = `${currentPath}/:${node.paramChild.paramName}`;
104
+ routes.push(
105
+ ...collectRoutes(
106
+ node.paramChild,
107
+ paramPath.replace(`/${node.paramChild.segment}`, ""),
108
+ ),
109
+ );
110
+ }
111
+
112
+ if (node.wildcardChild) {
113
+ const wcPath = `${currentPath}/*${node.wildcardChild.paramName}`;
114
+ routes.push(
115
+ ...collectRoutes(
116
+ node.wildcardChild,
117
+ wcPath.replace(`/${node.wildcardChild.segment}`, ""),
118
+ ),
119
+ );
120
+ }
121
+
122
+ return routes;
123
+ }
124
+
125
+ // ─── CIDR Check ──────────────────────────────────────────────
126
+
127
+ function ipToLong(ip: string): number {
128
+ const parts = ip.split(".").map(Number);
129
+ return (
130
+ ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0
131
+ );
132
+ }
133
+
134
+ function isIpAllowed(clientIp: string, allowlist: string[]): boolean {
135
+ if (allowlist.length === 0) return true;
136
+ const clientLong = ipToLong(clientIp);
137
+
138
+ for (const entry of allowlist) {
139
+ if (entry.includes("/")) {
140
+ const [network, bits] = entry.split("/");
141
+ const mask = (~0 << (32 - Number(bits))) >>> 0;
142
+ if ((clientLong & mask) === (ipToLong(network) & mask)) return true;
143
+ } else {
144
+ if (clientIp === entry) return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+
150
+ // ─── Percentile Calculation ──────────────────────────────────
151
+
152
+ function percentile(sorted: number[], p: number): number {
153
+ if (sorted.length === 0) return 0;
154
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
155
+ return sorted[Math.max(0, idx)];
156
+ }
157
+
158
+ // ─── Debug Plugin Factory ────────────────────────────────────
159
+
160
+ /**
161
+ * Create a debug sidecar plugin that exposes introspection endpoints
162
+ * on a separate port.
163
+ *
164
+ * Development mode (default): no auth required, binds to 0.0.0.0.
165
+ * Production mode (opt-in): requires apiKey, supports IP allowlist,
166
+ * binds to 127.0.0.1 by default.
167
+ */
168
+ export function debugPlugin(options: DebugPluginOptions = {}): TypoKitPlugin {
169
+ const port = options.port ?? 9800;
170
+ const isProduction = options.production ?? false;
171
+ const security = options.security ?? {};
172
+ const redactPatterns = security.redact ?? [];
173
+ const rateLimit = security.rateLimit ?? 0;
174
+ const rateLimitWindow = security.rateLimitWindow ?? 60_000;
175
+
176
+ // Internal state
177
+ let cachedRoutes: RouteInfo[] = [];
178
+ let middlewareNames: string[] = [];
179
+ const recentErrors: ErrorRecord[] = [];
180
+ const recentTraces: SpanData[][] = [];
181
+ const recentLogs: LogEntry[] = [];
182
+ const performanceData: HistogramDataPoint[] = [];
183
+ let serverHandle: ServerHandle | null = null;
184
+ let dependencies: Record<string, string[]> = {};
185
+
186
+ // Rate limiting state
187
+ const rateLimitMap = new Map<string, RateLimitEntry>();
188
+
189
+ function checkRateLimit(clientIp: string): boolean {
190
+ if (rateLimit <= 0) return true;
191
+ const now = Date.now();
192
+ const entry = rateLimitMap.get(clientIp);
193
+ if (!entry || now >= entry.resetAt) {
194
+ rateLimitMap.set(clientIp, { count: 1, resetAt: now + rateLimitWindow });
195
+ return true;
196
+ }
197
+ entry.count++;
198
+ return entry.count <= rateLimit;
199
+ }
200
+
201
+ function getClientIp(req: TypoKitRequest): string {
202
+ const forwarded = req.headers["x-forwarded-for"];
203
+ if (typeof forwarded === "string") return forwarded.split(",")[0].trim();
204
+ return "127.0.0.1";
205
+ }
206
+
207
+ // Security middleware
208
+ function checkSecurity(req: TypoKitRequest): TypoKitResponse | null {
209
+ if (!isProduction) return null;
210
+
211
+ // API key check
212
+ if (security.apiKey) {
213
+ const key = req.headers["x-debug-key"];
214
+ if (key !== security.apiKey) {
215
+ return {
216
+ status: 401,
217
+ headers: { "content-type": "application/json" },
218
+ body: {
219
+ error: {
220
+ code: "UNAUTHORIZED",
221
+ message: "Invalid or missing X-Debug-Key header",
222
+ },
223
+ },
224
+ };
225
+ }
226
+ }
227
+
228
+ // IP allowlist check
229
+ if (security.allowlist && security.allowlist.length > 0) {
230
+ const clientIp = getClientIp(req);
231
+ if (!isIpAllowed(clientIp, security.allowlist)) {
232
+ return {
233
+ status: 403,
234
+ headers: { "content-type": "application/json" },
235
+ body: { error: { code: "FORBIDDEN", message: "IP not allowed" } },
236
+ };
237
+ }
238
+ }
239
+
240
+ // Rate limiting
241
+ if (rateLimit > 0) {
242
+ const clientIp = getClientIp(req);
243
+ if (!checkRateLimit(clientIp)) {
244
+ return {
245
+ status: 429,
246
+ headers: { "content-type": "application/json" },
247
+ body: {
248
+ error: { code: "RATE_LIMITED", message: "Too many requests" },
249
+ },
250
+ };
251
+ }
252
+ }
253
+
254
+ return null;
255
+ }
256
+
257
+ function maybeRedact(data: Record<string, unknown>): Record<string, unknown> {
258
+ if (redactPatterns.length === 0) return data;
259
+ return redactFields(data, redactPatterns);
260
+ }
261
+
262
+ function parseDuration(value: string | string[] | undefined): number {
263
+ if (!value || Array.isArray(value)) return 300_000; // default 5 min
264
+ const match = value.match(/^(\d+)(ms|s|m|h)?$/);
265
+ if (!match) return 300_000;
266
+ const num = Number(match[1]);
267
+ switch (match[2]) {
268
+ case "ms":
269
+ return num;
270
+ case "s":
271
+ return num * 1000;
272
+ case "m":
273
+ return num * 60_000;
274
+ case "h":
275
+ return num * 3_600_000;
276
+ default:
277
+ return num * 1000; // default to seconds
278
+ }
279
+ }
280
+
281
+ // Endpoint handlers
282
+ const endpoints: Record<string, (req: TypoKitRequest) => unknown> = {
283
+ "/_debug/routes": () => {
284
+ return { routes: cachedRoutes };
285
+ },
286
+
287
+ "/_debug/middleware": () => {
288
+ return { middleware: middlewareNames };
289
+ },
290
+
291
+ "/_debug/performance": (req) => {
292
+ const windowMs = parseDuration(req.query["window"] as string | undefined);
293
+ const cutoff = new Date(Date.now() - windowMs).toISOString();
294
+ const relevant = performanceData.filter((d) => d.timestamp >= cutoff);
295
+ const durations = relevant.map((d) => d.value).sort((a, b) => a - b);
296
+
297
+ return {
298
+ window: `${windowMs}ms`,
299
+ count: durations.length,
300
+ p50: percentile(durations, 50),
301
+ p95: percentile(durations, 95),
302
+ p99: percentile(durations, 99),
303
+ min: durations.length > 0 ? durations[0] : 0,
304
+ max: durations.length > 0 ? durations[durations.length - 1] : 0,
305
+ };
306
+ },
307
+
308
+ "/_debug/errors": (req) => {
309
+ const sinceMs = parseDuration(req.query["since"] as string | undefined);
310
+ const cutoff = new Date(Date.now() - sinceMs).toISOString();
311
+ const filtered = recentErrors.filter((e) => e.timestamp >= cutoff);
312
+
313
+ return {
314
+ errors: filtered.map((e) =>
315
+ redactPatterns.length > 0
316
+ ? {
317
+ ...e,
318
+ ...(e.details ? { details: maybeRedact(e.details) } : {}),
319
+ }
320
+ : e,
321
+ ),
322
+ };
323
+ },
324
+
325
+ "/_debug/health": () => {
326
+ const proc = (
327
+ globalThis as unknown as {
328
+ process?: {
329
+ memoryUsage?: () => {
330
+ heapUsed: number;
331
+ heapTotal: number;
332
+ rss: number;
333
+ };
334
+ };
335
+ }
336
+ ).process;
337
+ const mem = proc?.memoryUsage?.();
338
+
339
+ return {
340
+ status: "ok",
341
+ uptime: Date.now(),
342
+ memory: mem
343
+ ? { heapUsed: mem.heapUsed, heapTotal: mem.heapTotal, rss: mem.rss }
344
+ : null,
345
+ };
346
+ },
347
+
348
+ "/_debug/dependencies": () => {
349
+ return { dependencies };
350
+ },
351
+
352
+ "/_debug/traces": () => {
353
+ return {
354
+ traces: recentTraces.slice(-100).map((spans) =>
355
+ spans.map((s) =>
356
+ redactPatterns.length > 0
357
+ ? {
358
+ ...s,
359
+ attributes: maybeRedact(
360
+ s.attributes as unknown as Record<string, unknown>,
361
+ ) as unknown as Record<string, string | number | boolean>,
362
+ }
363
+ : s,
364
+ ),
365
+ ),
366
+ };
367
+ },
368
+
369
+ "/_debug/logs": (req) => {
370
+ const sinceMs = parseDuration(req.query["since"] as string | undefined);
371
+ const cutoff = new Date(Date.now() - sinceMs).toISOString();
372
+ const filtered = recentLogs.filter((l) => l.timestamp >= cutoff);
373
+
374
+ return {
375
+ logs: filtered.map((l) =>
376
+ redactPatterns.length > 0 && l.data
377
+ ? { ...l, data: maybeRedact(l.data) }
378
+ : l,
379
+ ),
380
+ };
381
+ },
382
+ };
383
+
384
+ async function handleRequest(req: TypoKitRequest): Promise<TypoKitResponse> {
385
+ // Only GET requests allowed (read-only)
386
+ if (req.method !== "GET") {
387
+ return {
388
+ status: 405,
389
+ headers: { "content-type": "application/json", allow: "GET" },
390
+ body: {
391
+ error: {
392
+ code: "METHOD_NOT_ALLOWED",
393
+ message: "Debug endpoints are read-only",
394
+ },
395
+ },
396
+ };
397
+ }
398
+
399
+ // Security check
400
+ const secError = checkSecurity(req);
401
+ if (secError) return secError;
402
+
403
+ const handler = endpoints[req.path];
404
+ if (!handler) {
405
+ return {
406
+ status: 404,
407
+ headers: { "content-type": "application/json" },
408
+ body: {
409
+ error: {
410
+ code: "NOT_FOUND",
411
+ message: `Unknown debug endpoint: ${req.path}`,
412
+ },
413
+ },
414
+ };
415
+ }
416
+
417
+ const body = handler(req);
418
+ return {
419
+ status: 200,
420
+ headers: { "content-type": "application/json" },
421
+ body,
422
+ };
423
+ }
424
+
425
+ const plugin: TypoKitPlugin = {
426
+ name: "plugin-debug",
427
+
428
+ async onStart(app: AppInstance): Promise<void> {
429
+ // Collect middleware names from plugins
430
+ middlewareNames = app.plugins
431
+ .filter((p) => p.name !== "plugin-debug")
432
+ .map((p) => p.name);
433
+
434
+ // Build dependency graph from services
435
+ dependencies = {};
436
+ for (const [key, value] of Object.entries(app.services)) {
437
+ if (
438
+ typeof value === "object" &&
439
+ value !== null &&
440
+ "dependencies" in value
441
+ ) {
442
+ dependencies[key] = (
443
+ value as { dependencies: string[] }
444
+ ).dependencies;
445
+ }
446
+ }
447
+
448
+ // Expose data collection APIs via services
449
+ app.services["_debug"] = {
450
+ recordError: (error: AppError, route?: string) => {
451
+ recentErrors.push({
452
+ timestamp: new Date().toISOString(),
453
+ code: error.code,
454
+ status: error.status,
455
+ message: error.message,
456
+ details: error.details,
457
+ route,
458
+ });
459
+ // Keep at most 1000 errors
460
+ if (recentErrors.length > 1000)
461
+ recentErrors.splice(0, recentErrors.length - 1000);
462
+ },
463
+ recordTrace: (spans: SpanData[]) => {
464
+ recentTraces.push(spans);
465
+ if (recentTraces.length > 500)
466
+ recentTraces.splice(0, recentTraces.length - 500);
467
+ },
468
+ recordLog: (entry: LogEntry) => {
469
+ recentLogs.push(entry);
470
+ if (recentLogs.length > 2000)
471
+ recentLogs.splice(0, recentLogs.length - 2000);
472
+ },
473
+ recordPerformance: (dataPoint: HistogramDataPoint) => {
474
+ performanceData.push(dataPoint);
475
+ if (performanceData.length > 5000)
476
+ performanceData.splice(0, performanceData.length - 5000);
477
+ },
478
+ setRouteTable: (routeTable: CompiledRouteTable) => {
479
+ cachedRoutes = collectRoutes(routeTable, "");
480
+ },
481
+ setMiddleware: (names: string[]) => {
482
+ middlewareNames = names;
483
+ },
484
+ };
485
+ },
486
+
487
+ async onReady(_app: AppInstance): Promise<void> {
488
+ const hostname = isProduction
489
+ ? (security.hostname ?? "127.0.0.1")
490
+ : "0.0.0.0";
491
+
492
+ const srv = createServer(handleRequest, { hostname });
493
+ serverHandle = await srv.listen(port);
494
+ },
495
+
496
+ onError(error: AppError, ctx: RequestContext): void {
497
+ recentErrors.push({
498
+ timestamp: new Date().toISOString(),
499
+ code: error.code,
500
+ status: error.status,
501
+ message: error.message,
502
+ details: error.details,
503
+ route: ctx.requestId,
504
+ });
505
+ if (recentErrors.length > 1000)
506
+ recentErrors.splice(0, recentErrors.length - 1000);
507
+ },
508
+
509
+ async onStop(_app: AppInstance): Promise<void> {
510
+ if (serverHandle) {
511
+ await serverHandle.close();
512
+ serverHandle = null;
513
+ }
514
+ },
515
+
516
+ onSchemaChange(_changes: SchemaChange[]): void {
517
+ // Route map will be refreshed by the next build cycle calling setRouteTable
518
+ // Clear cached routes so they'll be re-populated
519
+ cachedRoutes = [];
520
+ },
521
+ };
522
+
523
+ return plugin;
524
+ }