@nifrajs/devtools 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -53
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +104 -55
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,17 @@ export interface DevToolsClientOptions {
|
|
|
58
58
|
* When configuring `tracing()` yourself, register it before this plugin so DevTools attaches to
|
|
59
59
|
* that request owner.
|
|
60
60
|
*/
|
|
61
|
+
/** Query for a DevTools state snapshot: an optional `path` prefix and a most-recent-`limit`. */
|
|
62
|
+
export interface DevToolsQuery {
|
|
63
|
+
readonly path?: string | undefined;
|
|
64
|
+
readonly limit?: number | undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Filter a DevTools event buffer for a snapshot query - by `path` prefix and/or the most recent
|
|
68
|
+
* `limit`. Pure, so the `/state` endpoint and its tests (and the `nifra_inspect` MCP tool that reads
|
|
69
|
+
* that endpoint) share ONE definition of what a query returns.
|
|
70
|
+
*/
|
|
71
|
+
export declare function filterDevToolsEvents(events: readonly DevToolsEvent[], query?: DevToolsQuery): DevToolsEvent[];
|
|
61
72
|
export declare function devtools(options?: DevToolsOptions | undefined): import("@nifrajs/core/server").NifraPlugin<import("@nifrajs/core/server").AnyServer, import("@nifrajs/core/server").AnyServer>;
|
|
62
73
|
/**
|
|
63
74
|
* Returns a self-contained JavaScript string that creates a floating DevTools
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAcH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,wDAAwD;IACxD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5C,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACtC,oDAAoD;IACpD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC1C,gGAAgG;IAChG,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IACvD,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;IACnF;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7C;AAED,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAQD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAcH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,wDAAwD;IACxD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5C,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACtC,oDAAoD;IACpD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC1C,gGAAgG;IAChG,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IACvD,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;IACnF;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7C;AAED,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAQD;;;;;GAKG;AACH,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACpC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,KAAK,GAAE,aAAkB,GACxB,aAAa,EAAE,CASjB;AAED,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,kIA2L7D;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAuGhF"}
|
package/dist/index.js
CHANGED
|
@@ -20,15 +20,26 @@ import { tracing, } from "@nifrajs/otel";
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
const encoder = new TextEncoder();
|
|
22
22
|
/**
|
|
23
|
-
* DevTools
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
26
|
-
* that request owner.
|
|
23
|
+
* Filter a DevTools event buffer for a snapshot query - by `path` prefix and/or the most recent
|
|
24
|
+
* `limit`. Pure, so the `/state` endpoint and its tests (and the `nifra_inspect` MCP tool that reads
|
|
25
|
+
* that endpoint) share ONE definition of what a query returns.
|
|
27
26
|
*/
|
|
27
|
+
export function filterDevToolsEvents(events, query = {}) {
|
|
28
|
+
const { path, limit } = query;
|
|
29
|
+
const matched = path !== undefined ? events.filter((event) => event.path.startsWith(path)) : [...events];
|
|
30
|
+
// `slice(Math.max(0, n - limit))`, not `slice(-limit)`: the latter returns the WHOLE array for
|
|
31
|
+
// limit 0 (`-0 === 0`) and mishandles a limit larger than the buffer. This clamps both.
|
|
32
|
+
return limit !== undefined && limit >= 0
|
|
33
|
+
? matched.slice(Math.max(0, matched.length - limit))
|
|
34
|
+
: matched;
|
|
35
|
+
}
|
|
28
36
|
export function devtools(options) {
|
|
29
37
|
const maxEvents = options?.maxEvents ?? 200;
|
|
30
38
|
const maxConnections = Math.max(1, options?.maxConnections ?? 5);
|
|
31
39
|
const endpoint = options?.path ?? "/_nifra/devtools";
|
|
40
|
+
// A one-shot JSON snapshot of the ring buffer, alongside the live SSE stream. The SSE path is for a
|
|
41
|
+
// human watching the overlay; this is for an agent that made a request and wants to READ what happened.
|
|
42
|
+
const statePath = `${endpoint}/state`;
|
|
32
43
|
const enabled = options?.enabled ?? process.env.NODE_ENV === "development";
|
|
33
44
|
const allowRemote = options?.allowRemote ?? false;
|
|
34
45
|
const allowedOrigins = new Set(options?.allowedOrigins ?? []);
|
|
@@ -115,64 +126,81 @@ export function devtools(options) {
|
|
|
115
126
|
return app.use(tracing({ adapters: [adapter] })).use({
|
|
116
127
|
name: "devtools-stream",
|
|
117
128
|
async onRequest(request) {
|
|
118
|
-
// Handle the SSE endpoint
|
|
119
129
|
const url = new URL(request.url);
|
|
120
|
-
if (url.pathname
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Send buffered events as initial payload
|
|
148
|
-
for (const evt of buffer) {
|
|
149
|
-
try {
|
|
150
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(evt)}\n\n`));
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
cancel() {
|
|
158
|
-
if (activeController !== undefined)
|
|
159
|
-
dropController(activeController);
|
|
160
|
-
},
|
|
130
|
+
if (url.pathname !== endpoint && url.pathname !== statePath)
|
|
131
|
+
return undefined;
|
|
132
|
+
// One access-control gate for BOTH the live stream and the one-shot snapshot: a read of the
|
|
133
|
+
// buffer exposes the same request data as watching it live, so it is guarded identically -
|
|
134
|
+
// loopback-only unless `allowRemote`, origin-checked, and an optional `authorize` hook.
|
|
135
|
+
const loopback = url.hostname === "localhost" ||
|
|
136
|
+
url.hostname === "127.0.0.1" ||
|
|
137
|
+
url.hostname === "[::1]" ||
|
|
138
|
+
url.hostname.endsWith(".localhost");
|
|
139
|
+
if (!allowRemote && !loopback) {
|
|
140
|
+
return new Response("Nifra DevTools is restricted to loopback hosts", { status: 403 });
|
|
141
|
+
}
|
|
142
|
+
const origin = request.headers.get("origin");
|
|
143
|
+
if (origin !== null && origin !== url.origin && !allowedOrigins.has(origin)) {
|
|
144
|
+
return new Response("Origin not allowed", { status: 403 });
|
|
145
|
+
}
|
|
146
|
+
if (options?.authorize !== undefined && !(await options.authorize(request))) {
|
|
147
|
+
return new Response("Unauthorized", { status: 401 });
|
|
148
|
+
}
|
|
149
|
+
// The snapshot: recent request traces as JSON, filtered by ?path= / ?limit=. This is the agent's
|
|
150
|
+
// read of "what did my requests actually do" - the one thing the live SSE stream can't answer.
|
|
151
|
+
if (url.pathname === statePath) {
|
|
152
|
+
const limitRaw = url.searchParams.get("limit");
|
|
153
|
+
const limit = limitRaw !== null && Number.isFinite(Number(limitRaw)) ? Number(limitRaw) : undefined;
|
|
154
|
+
const events = filterDevToolsEvents(buffer, {
|
|
155
|
+
path: url.searchParams.get("path") ?? undefined,
|
|
156
|
+
limit,
|
|
161
157
|
});
|
|
162
|
-
|
|
163
|
-
const response = new Response(stream, {
|
|
158
|
+
return Response.json({ events }, {
|
|
164
159
|
headers: {
|
|
165
|
-
"content-type": "text/event-stream",
|
|
166
160
|
"cache-control": "no-store",
|
|
167
|
-
connection: "keep-alive",
|
|
168
|
-
"content-security-policy": "default-src 'none'",
|
|
169
161
|
"x-content-type-options": "nosniff",
|
|
170
162
|
"x-nifra-devtools": "true",
|
|
171
163
|
},
|
|
172
164
|
});
|
|
173
|
-
return response;
|
|
174
165
|
}
|
|
175
|
-
|
|
166
|
+
// The live SSE stream.
|
|
167
|
+
if (controllers.size >= maxConnections) {
|
|
168
|
+
return new Response("Too many DevTools connections", {
|
|
169
|
+
status: 503,
|
|
170
|
+
headers: { "retry-after": "5" },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
let activeController;
|
|
174
|
+
const stream = new ReadableStream({
|
|
175
|
+
start(controller) {
|
|
176
|
+
activeController = controller;
|
|
177
|
+
controllers.add(controller);
|
|
178
|
+
startPing();
|
|
179
|
+
// Send buffered events as the initial payload.
|
|
180
|
+
for (const evt of buffer) {
|
|
181
|
+
try {
|
|
182
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(evt)}\n\n`));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
cancel() {
|
|
190
|
+
if (activeController !== undefined)
|
|
191
|
+
dropController(activeController);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
return new Response(stream, {
|
|
195
|
+
headers: {
|
|
196
|
+
"content-type": "text/event-stream",
|
|
197
|
+
"cache-control": "no-store",
|
|
198
|
+
connection: "keep-alive",
|
|
199
|
+
"content-security-policy": "default-src 'none'",
|
|
200
|
+
"x-content-type-options": "nosniff",
|
|
201
|
+
"x-nifra-devtools": "true",
|
|
202
|
+
},
|
|
203
|
+
});
|
|
176
204
|
},
|
|
177
205
|
beforeHandle(context) {
|
|
178
206
|
;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAIL,OAAO,GACR,MAAM,eAAe,CAAA;AAgDtB,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/devtools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Interactive DevTools overlay for nifra dev - loader traces, ISR status, route metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@nifrajs/otel": "2.
|
|
34
|
+
"@nifrajs/otel": "2.5.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@nifrajs/core": "^2.
|
|
37
|
+
"@nifrajs/core": "^2.5.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@nifrajs/core": "workspace:*"
|
package/src/index.ts
CHANGED
|
@@ -80,10 +80,38 @@ const encoder = new TextEncoder()
|
|
|
80
80
|
* When configuring `tracing()` yourself, register it before this plugin so DevTools attaches to
|
|
81
81
|
* that request owner.
|
|
82
82
|
*/
|
|
83
|
+
/** Query for a DevTools state snapshot: an optional `path` prefix and a most-recent-`limit`. */
|
|
84
|
+
export interface DevToolsQuery {
|
|
85
|
+
readonly path?: string | undefined
|
|
86
|
+
readonly limit?: number | undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Filter a DevTools event buffer for a snapshot query - by `path` prefix and/or the most recent
|
|
91
|
+
* `limit`. Pure, so the `/state` endpoint and its tests (and the `nifra_inspect` MCP tool that reads
|
|
92
|
+
* that endpoint) share ONE definition of what a query returns.
|
|
93
|
+
*/
|
|
94
|
+
export function filterDevToolsEvents(
|
|
95
|
+
events: readonly DevToolsEvent[],
|
|
96
|
+
query: DevToolsQuery = {},
|
|
97
|
+
): DevToolsEvent[] {
|
|
98
|
+
const { path, limit } = query
|
|
99
|
+
const matched =
|
|
100
|
+
path !== undefined ? events.filter((event) => event.path.startsWith(path)) : [...events]
|
|
101
|
+
// `slice(Math.max(0, n - limit))`, not `slice(-limit)`: the latter returns the WHOLE array for
|
|
102
|
+
// limit 0 (`-0 === 0`) and mishandles a limit larger than the buffer. This clamps both.
|
|
103
|
+
return limit !== undefined && limit >= 0
|
|
104
|
+
? matched.slice(Math.max(0, matched.length - limit))
|
|
105
|
+
: matched
|
|
106
|
+
}
|
|
107
|
+
|
|
83
108
|
export function devtools(options?: DevToolsOptions | undefined) {
|
|
84
109
|
const maxEvents = options?.maxEvents ?? 200
|
|
85
110
|
const maxConnections = Math.max(1, options?.maxConnections ?? 5)
|
|
86
111
|
const endpoint = options?.path ?? "/_nifra/devtools"
|
|
112
|
+
// A one-shot JSON snapshot of the ring buffer, alongside the live SSE stream. The SSE path is for a
|
|
113
|
+
// human watching the overlay; this is for an agent that made a request and wants to READ what happened.
|
|
114
|
+
const statePath = `${endpoint}/state`
|
|
87
115
|
const enabled = options?.enabled ?? process.env.NODE_ENV === "development"
|
|
88
116
|
const allowRemote = options?.allowRemote ?? false
|
|
89
117
|
const allowedOrigins = new Set(options?.allowedOrigins ?? [])
|
|
@@ -172,67 +200,88 @@ export function devtools(options?: DevToolsOptions | undefined) {
|
|
|
172
200
|
name: "devtools-stream",
|
|
173
201
|
|
|
174
202
|
async onRequest(request: Request): Promise<Response | undefined> {
|
|
175
|
-
// Handle the SSE endpoint
|
|
176
203
|
const url = new URL(request.url)
|
|
177
|
-
if (url.pathname
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
let activeController: ReadableStreamDefaultController | undefined
|
|
201
|
-
const stream = new ReadableStream({
|
|
202
|
-
start(controller) {
|
|
203
|
-
activeController = controller
|
|
204
|
-
controllers.add(controller)
|
|
205
|
-
startPing()
|
|
206
|
-
// Send buffered events as initial payload
|
|
207
|
-
for (const evt of buffer) {
|
|
208
|
-
try {
|
|
209
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(evt)}\n\n`))
|
|
210
|
-
} catch {
|
|
211
|
-
break
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
cancel() {
|
|
216
|
-
if (activeController !== undefined) dropController(activeController)
|
|
217
|
-
},
|
|
218
|
-
})
|
|
204
|
+
if (url.pathname !== endpoint && url.pathname !== statePath) return undefined
|
|
205
|
+
|
|
206
|
+
// One access-control gate for BOTH the live stream and the one-shot snapshot: a read of the
|
|
207
|
+
// buffer exposes the same request data as watching it live, so it is guarded identically -
|
|
208
|
+
// loopback-only unless `allowRemote`, origin-checked, and an optional `authorize` hook.
|
|
209
|
+
const loopback =
|
|
210
|
+
url.hostname === "localhost" ||
|
|
211
|
+
url.hostname === "127.0.0.1" ||
|
|
212
|
+
url.hostname === "[::1]" ||
|
|
213
|
+
url.hostname.endsWith(".localhost")
|
|
214
|
+
if (!allowRemote && !loopback) {
|
|
215
|
+
return new Response("Nifra DevTools is restricted to loopback hosts", { status: 403 })
|
|
216
|
+
}
|
|
217
|
+
const origin = request.headers.get("origin")
|
|
218
|
+
if (origin !== null && origin !== url.origin && !allowedOrigins.has(origin)) {
|
|
219
|
+
return new Response("Origin not allowed", { status: 403 })
|
|
220
|
+
}
|
|
221
|
+
if (options?.authorize !== undefined && !(await options.authorize(request))) {
|
|
222
|
+
return new Response("Unauthorized", { status: 401 })
|
|
223
|
+
}
|
|
219
224
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
},
|
|
225
|
+
// The snapshot: recent request traces as JSON, filtered by ?path= / ?limit=. This is the agent's
|
|
226
|
+
// read of "what did my requests actually do" - the one thing the live SSE stream can't answer.
|
|
227
|
+
if (url.pathname === statePath) {
|
|
228
|
+
const limitRaw = url.searchParams.get("limit")
|
|
229
|
+
const limit =
|
|
230
|
+
limitRaw !== null && Number.isFinite(Number(limitRaw)) ? Number(limitRaw) : undefined
|
|
231
|
+
const events = filterDevToolsEvents(buffer, {
|
|
232
|
+
path: url.searchParams.get("path") ?? undefined,
|
|
233
|
+
limit,
|
|
230
234
|
})
|
|
235
|
+
return Response.json(
|
|
236
|
+
{ events },
|
|
237
|
+
{
|
|
238
|
+
headers: {
|
|
239
|
+
"cache-control": "no-store",
|
|
240
|
+
"x-content-type-options": "nosniff",
|
|
241
|
+
"x-nifra-devtools": "true",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
}
|
|
231
246
|
|
|
232
|
-
|
|
247
|
+
// The live SSE stream.
|
|
248
|
+
if (controllers.size >= maxConnections) {
|
|
249
|
+
return new Response("Too many DevTools connections", {
|
|
250
|
+
status: 503,
|
|
251
|
+
headers: { "retry-after": "5" },
|
|
252
|
+
})
|
|
233
253
|
}
|
|
234
254
|
|
|
235
|
-
|
|
255
|
+
let activeController: ReadableStreamDefaultController | undefined
|
|
256
|
+
const stream = new ReadableStream({
|
|
257
|
+
start(controller) {
|
|
258
|
+
activeController = controller
|
|
259
|
+
controllers.add(controller)
|
|
260
|
+
startPing()
|
|
261
|
+
// Send buffered events as the initial payload.
|
|
262
|
+
for (const evt of buffer) {
|
|
263
|
+
try {
|
|
264
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(evt)}\n\n`))
|
|
265
|
+
} catch {
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
cancel() {
|
|
271
|
+
if (activeController !== undefined) dropController(activeController)
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
return new Response(stream, {
|
|
276
|
+
headers: {
|
|
277
|
+
"content-type": "text/event-stream",
|
|
278
|
+
"cache-control": "no-store",
|
|
279
|
+
connection: "keep-alive",
|
|
280
|
+
"content-security-policy": "default-src 'none'",
|
|
281
|
+
"x-content-type-options": "nosniff",
|
|
282
|
+
"x-nifra-devtools": "true",
|
|
283
|
+
},
|
|
284
|
+
})
|
|
236
285
|
},
|
|
237
286
|
|
|
238
287
|
beforeHandle(context) {
|