@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.
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +382 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
- package/src/index.test.ts +342 -0
- package/src/index.ts +524 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|