@beignet/devtools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +464 -0
  3. package/dist/access.d.ts +21 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +20 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/audit.d.ts +10 -0
  8. package/dist/audit.d.ts.map +1 -0
  9. package/dist/audit.js +49 -0
  10. package/dist/audit.js.map +1 -0
  11. package/dist/events.d.ts +143 -0
  12. package/dist/events.d.ts.map +1 -0
  13. package/dist/events.js +20 -0
  14. package/dist/events.js.map +1 -0
  15. package/dist/index.d.ts +114 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +44 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/instrumentation.d.ts +74 -0
  20. package/dist/instrumentation.d.ts.map +1 -0
  21. package/dist/instrumentation.js +293 -0
  22. package/dist/instrumentation.js.map +1 -0
  23. package/dist/persistence.d.ts +30 -0
  24. package/dist/persistence.d.ts.map +1 -0
  25. package/dist/persistence.js +100 -0
  26. package/dist/persistence.js.map +1 -0
  27. package/dist/provider-instrumentation.d.ts +9 -0
  28. package/dist/provider-instrumentation.d.ts.map +1 -0
  29. package/dist/provider-instrumentation.js +25 -0
  30. package/dist/provider-instrumentation.js.map +1 -0
  31. package/dist/provider.d.ts +79 -0
  32. package/dist/provider.d.ts.map +1 -0
  33. package/dist/provider.js +293 -0
  34. package/dist/provider.js.map +1 -0
  35. package/dist/redaction.d.ts +5 -0
  36. package/dist/redaction.d.ts.map +1 -0
  37. package/dist/redaction.js +20 -0
  38. package/dist/redaction.js.map +1 -0
  39. package/dist/routes.d.ts +113 -0
  40. package/dist/routes.d.ts.map +1 -0
  41. package/dist/routes.js +247 -0
  42. package/dist/routes.js.map +1 -0
  43. package/dist/trace-context.d.ts +29 -0
  44. package/dist/trace-context.d.ts.map +1 -0
  45. package/dist/trace-context.js +74 -0
  46. package/dist/trace-context.js.map +1 -0
  47. package/dist/ui.d.ts +14 -0
  48. package/dist/ui.d.ts.map +1 -0
  49. package/dist/ui.js +795 -0
  50. package/dist/ui.js.map +1 -0
  51. package/dist/watchers.d.ts +22 -0
  52. package/dist/watchers.d.ts.map +1 -0
  53. package/dist/watchers.js +171 -0
  54. package/dist/watchers.js.map +1 -0
  55. package/package.json +66 -0
  56. package/src/access.ts +52 -0
  57. package/src/audit.ts +71 -0
  58. package/src/events.ts +193 -0
  59. package/src/index.ts +136 -0
  60. package/src/instrumentation.ts +451 -0
  61. package/src/persistence.ts +163 -0
  62. package/src/provider-instrumentation.ts +50 -0
  63. package/src/provider.ts +375 -0
  64. package/src/redaction.ts +26 -0
  65. package/src/routes.ts +317 -0
  66. package/src/trace-context.ts +115 -0
  67. package/src/ui.ts +807 -0
  68. package/src/watchers.ts +235 -0
package/src/routes.ts ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * HTTP handlers for devtools API
3
+ *
4
+ * These handlers can be used to expose devtools data via HTTP endpoints
5
+ * in Next.js or any other framework that uses the Fetch API Request/Response.
6
+ */
7
+
8
+ import {
9
+ authorizeDevtoolsRequest,
10
+ type DevtoolsRouteAccessOptions,
11
+ } from "./access";
12
+ import { isDevtoolsEventType } from "./events";
13
+ import type { DevtoolsFilter, DevtoolsPort } from "./index";
14
+ import { handleDevtoolsUIRequest } from "./ui";
15
+
16
+ export interface DevtoolsRequestOptions extends DevtoolsRouteAccessOptions {
17
+ /**
18
+ * URL path prefix where devtools routes are mounted.
19
+ *
20
+ * @example "/api/devtools"
21
+ */
22
+ basePath: string;
23
+ }
24
+
25
+ export interface DevtoolsRouteHandlers {
26
+ GET(req: Request): Promise<Response>;
27
+ POST(req: Request): Promise<Response>;
28
+ }
29
+
30
+ /**
31
+ * Handle GET requests to list devtools events with optional filtering.
32
+ *
33
+ * Query parameters:
34
+ * - type: Filter by event type (request, error, usecase, eventBus, job, schedule, provider)
35
+ * - requestId: Filter by request correlation ID
36
+ * - traceId: Filter by W3C trace ID
37
+ * - limit: Maximum number of events to return (default: 200)
38
+ *
39
+ * @param req - The incoming HTTP request
40
+ * @param devtools - The DevtoolsPort instance to query
41
+ * @returns JSON response with events array
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // In a Next.js route handler:
46
+ * // app/api/devtools/core/events/route.ts
47
+ * import { handleDevtoolsEventsRequest } from "@beignet/devtools";
48
+ *
49
+ * export async function GET(req: Request) {
50
+ * const devtools = getAppDevtoolsPort(); // app-specific helper
51
+ * return handleDevtoolsEventsRequest(req, devtools);
52
+ * }
53
+ * ```
54
+ */
55
+ export async function handleDevtoolsEventsRequest(
56
+ req: Request,
57
+ devtools: DevtoolsPort,
58
+ options: DevtoolsRouteAccessOptions = {},
59
+ ): Promise<Response> {
60
+ const denied = await authorizeDevtoolsRequest(req, options);
61
+ if (denied) return denied;
62
+
63
+ const url = new URL(req.url);
64
+ const type = url.searchParams.get("type");
65
+ const requestId = url.searchParams.get("requestId");
66
+ const traceId = url.searchParams.get("traceId");
67
+ const limitParam = url.searchParams.get("limit");
68
+
69
+ if (type !== null && !isDevtoolsEventType(type)) {
70
+ return new Response(JSON.stringify({ error: "Invalid event type" }), {
71
+ status: 400,
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ "Cache-Control": "no-store",
75
+ },
76
+ });
77
+ }
78
+
79
+ let limit: number | undefined;
80
+ if (limitParam !== null) {
81
+ const parsed = Number.parseInt(limitParam, 10);
82
+ if (!Number.isNaN(parsed)) {
83
+ limit = parsed;
84
+ }
85
+ }
86
+
87
+ const filter: DevtoolsFilter = {
88
+ type: type ?? undefined,
89
+ requestId: requestId ?? undefined,
90
+ traceId: traceId ?? undefined,
91
+ limit,
92
+ };
93
+
94
+ const events = devtools.getEvents(filter);
95
+
96
+ return new Response(
97
+ JSON.stringify({
98
+ events,
99
+ watchers: devtools.getWatchers(),
100
+ }),
101
+ {
102
+ status: 200,
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ "Cache-Control": "no-store",
106
+ },
107
+ },
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Handle POST requests to clear all devtools events.
113
+ *
114
+ * This endpoint requires a POST request and will clear the in-memory event buffer.
115
+ *
116
+ * @param req - The incoming HTTP request
117
+ * @param devtools - The DevtoolsPort instance to clear
118
+ * @returns JSON response with success status
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * // In a Next.js route handler:
123
+ * // app/api/devtools/clear/route.ts
124
+ * import { handleDevtoolsClearRequest } from "@beignet/devtools";
125
+ *
126
+ * export async function POST(req: Request) {
127
+ * const devtools = getAppDevtoolsPort(); // app-specific helper
128
+ * return handleDevtoolsClearRequest(req, devtools);
129
+ * }
130
+ * ```
131
+ */
132
+ export async function handleDevtoolsClearRequest(
133
+ req: Request,
134
+ devtools: DevtoolsPort,
135
+ options: DevtoolsRouteAccessOptions = {},
136
+ ): Promise<Response> {
137
+ const denied = await authorizeDevtoolsRequest(req, options);
138
+ if (denied) return denied;
139
+
140
+ if (req.method !== "POST") {
141
+ return new Response("Method Not Allowed", { status: 405 });
142
+ }
143
+
144
+ await devtools.clear();
145
+
146
+ return new Response(JSON.stringify({ ok: true }), {
147
+ status: 200,
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ "Cache-Control": "no-store",
151
+ },
152
+ });
153
+ }
154
+
155
+ function encodeSse(event: string, data: unknown): Uint8Array {
156
+ return new TextEncoder().encode(
157
+ `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`,
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Handle GET requests for a live Server-Sent Events stream.
163
+ *
164
+ * The stream emits:
165
+ * - `snapshot` with the current event buffer
166
+ * - `event` when a new event is recorded
167
+ * - `clear` when the buffer is cleared
168
+ */
169
+ export async function handleDevtoolsStreamRequest(
170
+ req: Request,
171
+ devtools: DevtoolsPort,
172
+ options: DevtoolsRouteAccessOptions = {},
173
+ ): Promise<Response> {
174
+ const denied = await authorizeDevtoolsRequest(req, options);
175
+ if (denied) return denied;
176
+
177
+ if (req.method !== "GET") {
178
+ return new Response("Method Not Allowed", { status: 405 });
179
+ }
180
+
181
+ let unsubscribe: (() => void) | undefined;
182
+
183
+ const stream = new ReadableStream<Uint8Array>({
184
+ start(controller) {
185
+ let closed = false;
186
+
187
+ const enqueue = (event: string, data: unknown) => {
188
+ if (closed) return;
189
+ try {
190
+ controller.enqueue(encodeSse(event, data));
191
+ } catch {
192
+ closed = true;
193
+ }
194
+ };
195
+
196
+ enqueue("snapshot", {
197
+ events: devtools.getEvents({ limit: 500 }),
198
+ watchers: devtools.getWatchers(),
199
+ });
200
+
201
+ unsubscribe = devtools.subscribe((event) => {
202
+ if (event.type === "record") {
203
+ enqueue("event", { event: event.event });
204
+ return;
205
+ }
206
+ enqueue("clear", { ok: true });
207
+ });
208
+
209
+ req.signal.addEventListener(
210
+ "abort",
211
+ () => {
212
+ closed = true;
213
+ unsubscribe?.();
214
+ try {
215
+ controller.close();
216
+ } catch {
217
+ // The stream may already be closed by the client.
218
+ }
219
+ },
220
+ { once: true },
221
+ );
222
+ },
223
+ cancel() {
224
+ unsubscribe?.();
225
+ },
226
+ });
227
+
228
+ return new Response(stream, {
229
+ status: 200,
230
+ headers: {
231
+ "Content-Type": "text/event-stream; charset=utf-8",
232
+ "Cache-Control": "no-store, no-transform",
233
+ Connection: "keep-alive",
234
+ },
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Unified devtools request handler.
240
+ *
241
+ * Routes requests to the appropriate handler based on the URL path:
242
+ * - `{basePath}/core/events` → event list (GET)
243
+ * - `{basePath}/stream` → live event stream (GET)
244
+ * - `{basePath}/clear` → clear buffer (POST)
245
+ * - `{basePath}` → dashboard UI (GET)
246
+ *
247
+ * This lets you wire up a single catch-all route instead of three separate ones.
248
+ *
249
+ * @param req - The incoming HTTP request
250
+ * @param devtools - The DevtoolsPort instance
251
+ * @param options - Devtools route options including base path and access policy
252
+ * @returns Response for the matched sub-route
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * // Next.js catch-all route: app/api/devtools/[[...path]]/route.ts
257
+ * import { createDevtoolsRoute } from "@beignet/devtools";
258
+ * import { getDevtools } from "@/server";
259
+ *
260
+ * export const { GET, POST } = createDevtoolsRoute(getDevtools(), {
261
+ * basePath: "/api/devtools",
262
+ * });
263
+ * ```
264
+ */
265
+ export async function handleDevtoolsRequest(
266
+ req: Request,
267
+ devtools: DevtoolsPort,
268
+ options: DevtoolsRequestOptions,
269
+ ): Promise<Response> {
270
+ const denied = await authorizeDevtoolsRequest(req, options);
271
+ if (denied) return denied;
272
+
273
+ const url = new URL(req.url);
274
+ const path = url.pathname.replace(/\/+$/, "");
275
+ const normalizedBase = options.basePath.replace(/\/+$/, "");
276
+ const accessChecked: DevtoolsRouteAccessOptions = { enabled: true };
277
+
278
+ if (path === `${normalizedBase}/core/events`) {
279
+ return handleDevtoolsEventsRequest(req, devtools, accessChecked);
280
+ }
281
+
282
+ if (path === `${normalizedBase}/clear`) {
283
+ return handleDevtoolsClearRequest(req, devtools, accessChecked);
284
+ }
285
+
286
+ if (path === `${normalizedBase}/stream`) {
287
+ return handleDevtoolsStreamRequest(req, devtools, accessChecked);
288
+ }
289
+
290
+ if (path === normalizedBase) {
291
+ if (req.method !== "GET") {
292
+ return new Response("Method Not Allowed", { status: 405 });
293
+ }
294
+ return handleDevtoolsUIRequest(options.basePath, accessChecked);
295
+ }
296
+
297
+ return new Response("Not Found", { status: 404 });
298
+ }
299
+
300
+ /**
301
+ * Create GET and POST handlers for a devtools catch-all route.
302
+ *
303
+ * This is the recommended integration for frameworks such as Next.js that
304
+ * export HTTP method functions from route modules.
305
+ */
306
+ export function createDevtoolsRoute(
307
+ devtools: DevtoolsPort,
308
+ options: DevtoolsRequestOptions,
309
+ ): DevtoolsRouteHandlers {
310
+ const handle = (req: Request) =>
311
+ handleDevtoolsRequest(req, devtools, options);
312
+
313
+ return {
314
+ GET: handle,
315
+ POST: handle,
316
+ };
317
+ }
@@ -0,0 +1,115 @@
1
+ const TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
2
+
3
+ export interface DevtoolsTraceContext {
4
+ traceId: string;
5
+ spanId: string;
6
+ parentSpanId?: string;
7
+ traceparent: string;
8
+ }
9
+
10
+ export interface ParsedTraceparent {
11
+ traceId: string;
12
+ spanId: string;
13
+ traceFlags: string;
14
+ traceparent: string;
15
+ }
16
+
17
+ export interface DevtoolsTraceContextInput {
18
+ traceId?: string;
19
+ spanId?: string;
20
+ parentSpanId?: string;
21
+ traceparent?: string;
22
+ }
23
+
24
+ function createHexId(length: number): string {
25
+ const bytes = new Uint8Array(length / 2);
26
+ if (typeof crypto !== "undefined" && "getRandomValues" in crypto) {
27
+ crypto.getRandomValues(bytes);
28
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
29
+ "",
30
+ );
31
+ }
32
+
33
+ let value = "";
34
+ while (value.length < length) {
35
+ value += Math.floor(Math.random() * 16).toString(16);
36
+ }
37
+ return value.slice(0, length);
38
+ }
39
+
40
+ function isNonZeroHex(value: string): boolean {
41
+ return !/^0+$/.test(value);
42
+ }
43
+
44
+ export function createTraceId(): string {
45
+ let traceId = createHexId(32);
46
+ while (!isNonZeroHex(traceId)) {
47
+ traceId = createHexId(32);
48
+ }
49
+ return traceId;
50
+ }
51
+
52
+ export function createSpanId(): string {
53
+ let spanId = createHexId(16);
54
+ while (!isNonZeroHex(spanId)) {
55
+ spanId = createHexId(16);
56
+ }
57
+ return spanId;
58
+ }
59
+
60
+ export function createTraceparent(args: {
61
+ traceId: string;
62
+ spanId: string;
63
+ traceFlags?: string;
64
+ }): string {
65
+ return `00-${args.traceId}-${args.spanId}-${args.traceFlags ?? "01"}`;
66
+ }
67
+
68
+ export function parseTraceparent(
69
+ value: string | null | undefined,
70
+ ): ParsedTraceparent | undefined {
71
+ if (!value) return undefined;
72
+ const normalized = value.trim().toLowerCase();
73
+ const match = TRACEPARENT_PATTERN.exec(normalized);
74
+ if (!match) return undefined;
75
+
76
+ const [, traceId, spanId, traceFlags] = match;
77
+ if (!isNonZeroHex(traceId) || !isNonZeroHex(spanId)) return undefined;
78
+
79
+ return {
80
+ traceId,
81
+ spanId,
82
+ traceFlags,
83
+ traceparent: normalized,
84
+ };
85
+ }
86
+
87
+ export function createDevtoolsTraceContext(
88
+ input: DevtoolsTraceContextInput = {},
89
+ ): DevtoolsTraceContext {
90
+ const parsed = parseTraceparent(input.traceparent);
91
+ const traceId = input.traceId ?? parsed?.traceId ?? createTraceId();
92
+ const parentSpanId = input.parentSpanId ?? parsed?.spanId;
93
+ const spanId = input.spanId ?? createSpanId();
94
+
95
+ return {
96
+ traceId,
97
+ spanId,
98
+ ...(parentSpanId ? { parentSpanId } : {}),
99
+ traceparent: createTraceparent({
100
+ traceId,
101
+ spanId,
102
+ traceFlags: parsed?.traceFlags,
103
+ }),
104
+ };
105
+ }
106
+
107
+ export function createChildDevtoolsTraceContext(
108
+ parent: DevtoolsTraceContextInput,
109
+ ): DevtoolsTraceContext {
110
+ return createDevtoolsTraceContext({
111
+ traceId: parent.traceId,
112
+ parentSpanId: parent.spanId,
113
+ traceparent: parent.traceparent,
114
+ });
115
+ }