@nifrajs/devtools 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -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,kIAmK7D;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAuGhF"}
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 plugin. Its observation adapter projects the single request span into a
24
- * `DevToolsEvent`; its middleware only owns the secured SSE transport.
25
- * When configuring `tracing()` yourself, register it before this plugin so DevTools attaches to
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 === endpoint) {
121
- const loopback = url.hostname === "localhost" ||
122
- url.hostname === "127.0.0.1" ||
123
- url.hostname === "[::1]" ||
124
- url.hostname.endsWith(".localhost");
125
- if (!allowRemote && !loopback) {
126
- return new Response("Nifra DevTools is restricted to loopback hosts", { status: 403 });
127
- }
128
- const origin = request.headers.get("origin");
129
- if (origin !== null && origin !== url.origin && !allowedOrigins.has(origin)) {
130
- return new Response("Origin not allowed", { status: 403 });
131
- }
132
- if (options?.authorize !== undefined && !(await options.authorize(request))) {
133
- return new Response("Unauthorized", { status: 401 });
134
- }
135
- if (controllers.size >= maxConnections) {
136
- return new Response("Too many DevTools connections", {
137
- status: 503,
138
- headers: { "retry-after": "5" },
139
- });
140
- }
141
- let activeController;
142
- const stream = new ReadableStream({
143
- start(controller) {
144
- activeController = controller;
145
- controllers.add(controller);
146
- startPing();
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
- // Clean up dead controllers on cancel
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
- return undefined;
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":"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"}
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.0",
3
+ "version": "2.4.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.3.0"
34
+ "@nifrajs/otel": "2.4.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@nifrajs/core": "^2.3.0"
37
+ "@nifrajs/core": "^2.4.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 === endpoint) {
178
- const loopback =
179
- url.hostname === "localhost" ||
180
- url.hostname === "127.0.0.1" ||
181
- url.hostname === "[::1]" ||
182
- url.hostname.endsWith(".localhost")
183
- if (!allowRemote && !loopback) {
184
- return new Response("Nifra DevTools is restricted to loopback hosts", { status: 403 })
185
- }
186
- const origin = request.headers.get("origin")
187
- if (origin !== null && origin !== url.origin && !allowedOrigins.has(origin)) {
188
- return new Response("Origin not allowed", { status: 403 })
189
- }
190
- if (options?.authorize !== undefined && !(await options.authorize(request))) {
191
- return new Response("Unauthorized", { status: 401 })
192
- }
193
- if (controllers.size >= maxConnections) {
194
- return new Response("Too many DevTools connections", {
195
- status: 503,
196
- headers: { "retry-after": "5" },
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
- // Clean up dead controllers on cancel
221
- const response = new Response(stream, {
222
- headers: {
223
- "content-type": "text/event-stream",
224
- "cache-control": "no-store",
225
- connection: "keep-alive",
226
- "content-security-policy": "default-src 'none'",
227
- "x-content-type-options": "nosniff",
228
- "x-nifra-devtools": "true",
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
- return response
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
- return undefined
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) {