@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.
- package/CHANGELOG.md +5 -0
- package/README.md +464 -0
- package/dist/access.d.ts +21 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +20 -0
- package/dist/access.js.map +1 -0
- package/dist/audit.d.ts +10 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +49 -0
- package/dist/audit.js.map +1 -0
- package/dist/events.d.ts +143 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +20 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +74 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +293 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/persistence.d.ts +30 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +100 -0
- package/dist/persistence.js.map +1 -0
- package/dist/provider-instrumentation.d.ts +9 -0
- package/dist/provider-instrumentation.d.ts.map +1 -0
- package/dist/provider-instrumentation.js +25 -0
- package/dist/provider-instrumentation.js.map +1 -0
- package/dist/provider.d.ts +79 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +293 -0
- package/dist/provider.js.map +1 -0
- package/dist/redaction.d.ts +5 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +20 -0
- package/dist/redaction.js.map +1 -0
- package/dist/routes.d.ts +113 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +247 -0
- package/dist/routes.js.map +1 -0
- package/dist/trace-context.d.ts +29 -0
- package/dist/trace-context.d.ts.map +1 -0
- package/dist/trace-context.js +74 -0
- package/dist/trace-context.js.map +1 -0
- package/dist/ui.d.ts +14 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +795 -0
- package/dist/ui.js.map +1 -0
- package/dist/watchers.d.ts +22 -0
- package/dist/watchers.d.ts.map +1 -0
- package/dist/watchers.js +171 -0
- package/dist/watchers.js.map +1 -0
- package/package.json +66 -0
- package/src/access.ts +52 -0
- package/src/audit.ts +71 -0
- package/src/events.ts +193 -0
- package/src/index.ts +136 -0
- package/src/instrumentation.ts +451 -0
- package/src/persistence.ts +163 -0
- package/src/provider-instrumentation.ts +50 -0
- package/src/provider.ts +375 -0
- package/src/redaction.ts +26 -0
- package/src/routes.ts +317 -0
- package/src/trace-context.ts +115 -0
- package/src/ui.ts +807 -0
- package/src/watchers.ts +235 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# @beignet/devtools
|
|
2
|
+
|
|
3
|
+
Development-time event timeline for Beignet apps. It records HTTP requests,
|
|
4
|
+
errors, use case runs, domain events, jobs, scheduled tasks, and provider
|
|
5
|
+
activity in a bounded in-memory buffer, then serves a live dashboard from your
|
|
6
|
+
app.
|
|
7
|
+
|
|
8
|
+
Devtools is enabled outside production by default and returns a no-op port in
|
|
9
|
+
production unless you explicitly enable it.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add @beignet/devtools
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Next.js setup
|
|
18
|
+
|
|
19
|
+
Register the provider and server hook:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import {
|
|
23
|
+
createDevtoolsHooks,
|
|
24
|
+
createDevtoolsProvider,
|
|
25
|
+
} from "@beignet/devtools";
|
|
26
|
+
import { createNextServer } from "@beignet/next";
|
|
27
|
+
|
|
28
|
+
export const server = await createNextServer({
|
|
29
|
+
ports,
|
|
30
|
+
providers: [createDevtoolsProvider(), ...providers],
|
|
31
|
+
hooks: [createDevtoolsHooks()],
|
|
32
|
+
createContext: ({ req, ports }) => ({
|
|
33
|
+
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
|
|
34
|
+
ports,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Add a catch-all route:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// app/api/devtools/[[...path]]/route.ts
|
|
43
|
+
import { createDevtoolsRoute } from "@beignet/devtools";
|
|
44
|
+
import { server } from "@/server";
|
|
45
|
+
|
|
46
|
+
export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
|
|
47
|
+
basePath: "/api/devtools",
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Open `/api/devtools`.
|
|
52
|
+
|
|
53
|
+
The dashboard connects to the event stream with Server-Sent Events and falls
|
|
54
|
+
back to polling when `EventSource` is unavailable. It includes tabs for the
|
|
55
|
+
timeline, requests, use cases, errors, domain events, jobs, schedules, providers, and
|
|
56
|
+
provider-owned events such as database/cache/storage/core/mail/auth/audit/rate limit
|
|
57
|
+
activity, and custom events.
|
|
58
|
+
Request rows expand into correlated events that share the same `traceId` or
|
|
59
|
+
`requestId`.
|
|
60
|
+
|
|
61
|
+
Use the toolbar to search across event summaries, paths, messages, names,
|
|
62
|
+
watchers, IDs, and details. The dashboard also includes method, status, and
|
|
63
|
+
watcher filters for narrowing noisy timelines.
|
|
64
|
+
|
|
65
|
+
Provider-owned tabs render focused panels with subsystem metrics and rows. For
|
|
66
|
+
example, database shows query/provider context, cache shows hit and failure
|
|
67
|
+
counts, auth shows authenticated versus guest activity, audit shows durable
|
|
68
|
+
activity records, and rate limits show allowed versus blocked checks.
|
|
69
|
+
|
|
70
|
+
## Local persistence
|
|
71
|
+
|
|
72
|
+
The default buffer is in memory. Enable local persistence when you want the
|
|
73
|
+
timeline to survive dev server restarts:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import {
|
|
77
|
+
createDevtoolsProvider,
|
|
78
|
+
createFileDevtoolsStore,
|
|
79
|
+
} from "@beignet/devtools";
|
|
80
|
+
|
|
81
|
+
createDevtoolsProvider({
|
|
82
|
+
store: createFileDevtoolsStore({
|
|
83
|
+
filePath: ".beignet/devtools/core/events.jsonl",
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You can also enable the built-in file store through environment variables:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
DEVTOOLS_PERSIST=true
|
|
92
|
+
DEVTOOLS_PERSIST_PATH=.beignet/devtools/core/events.jsonl
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The file store writes JSONL and compacts to the most recent configured events.
|
|
96
|
+
`POST /api/devtools/clear` clears the in-memory buffer and the configured store.
|
|
97
|
+
|
|
98
|
+
## Trace context
|
|
99
|
+
|
|
100
|
+
Devtools is OpenTelemetry-compatible without depending on the OpenTelemetry SDK.
|
|
101
|
+
`createDevtoolsHooks()` reads incoming W3C `traceparent` headers, creates a
|
|
102
|
+
local span when one is missing, exposes the current `traceparent` response
|
|
103
|
+
header, and adds trace fields to captured events.
|
|
104
|
+
|
|
105
|
+
All captured events can include:
|
|
106
|
+
|
|
107
|
+
- `traceId`: W3C trace ID for distributed correlation
|
|
108
|
+
- `spanId`: span ID for the operation represented by the event
|
|
109
|
+
- `parentSpanId`: parent span ID when the event is nested
|
|
110
|
+
- `traceparent`: W3C header value for the current span
|
|
111
|
+
|
|
112
|
+
For object contexts, the hook also adds these fields before the handler runs so
|
|
113
|
+
use case instrumentation can attach nested spans to the request trace.
|
|
114
|
+
|
|
115
|
+
## Watchers
|
|
116
|
+
|
|
117
|
+
Devtools is organized around watchers. A watcher owns one category of capture
|
|
118
|
+
and records typed events into the shared timeline.
|
|
119
|
+
|
|
120
|
+
Built-in watchers:
|
|
121
|
+
|
|
122
|
+
- `requests` records HTTP request timing and contract route activity.
|
|
123
|
+
- `errors` records unhandled errors, use case failures, and devtools failures.
|
|
124
|
+
- `useCases` records application command and query execution.
|
|
125
|
+
- `eventBus` records domain event publishing.
|
|
126
|
+
- `jobs` records background job lifecycle events.
|
|
127
|
+
- `schedules` records scheduled task execution.
|
|
128
|
+
- `providers` records provider setup, start, and stop activity.
|
|
129
|
+
- `db` records database diagnostics from first-party providers.
|
|
130
|
+
- `cache` records cache diagnostics from first-party providers.
|
|
131
|
+
- `storage` records storage diagnostics from first-party providers.
|
|
132
|
+
- `mail` records mail diagnostics from first-party providers.
|
|
133
|
+
- `auth` records auth diagnostics from first-party providers.
|
|
134
|
+
- `audit` records sanitized durable audit activity emitted by application code.
|
|
135
|
+
- `rateLimit` records rate limit diagnostics from first-party providers.
|
|
136
|
+
- `custom` records application and integration-specific diagnostic events.
|
|
137
|
+
|
|
138
|
+
Configure watchers through the provider:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
createDevtoolsProvider({
|
|
142
|
+
watchers: {
|
|
143
|
+
requests: true,
|
|
144
|
+
useCases: true,
|
|
145
|
+
eventBus: false,
|
|
146
|
+
jobs: false,
|
|
147
|
+
schedules: true,
|
|
148
|
+
db: true,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Disabled watchers do not store matching events. The installed watcher metadata
|
|
154
|
+
is available through `ctx.ports.devtools.getWatchers()` and the dashboard API.
|
|
155
|
+
|
|
156
|
+
Custom integrations can also register watcher metadata for their own event
|
|
157
|
+
types. Custom watcher tabs appear in the dashboard when they own `custom`
|
|
158
|
+
events:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
createDevtoolsProvider({
|
|
162
|
+
watchers: {
|
|
163
|
+
search: {
|
|
164
|
+
label: "Search",
|
|
165
|
+
description: "Search query and indexing diagnostics.",
|
|
166
|
+
eventTypes: ["custom"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Then record events with `watcher: "search"` so the custom watcher controls
|
|
173
|
+
whether they are stored.
|
|
174
|
+
|
|
175
|
+
## Use case instrumentation
|
|
176
|
+
|
|
177
|
+
Bridge the application package's `onRun` hook once in your shared use case
|
|
178
|
+
factory:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { createUseCase } from "@beignet/core/application";
|
|
182
|
+
import { createDevtoolsUseCaseObserver } from "@beignet/devtools";
|
|
183
|
+
|
|
184
|
+
export const useCase = createUseCase<AppContext>({
|
|
185
|
+
onRun: createDevtoolsUseCaseObserver<AppContext>(),
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The observer reads `ctx.ports.devtools`, `ctx.requestId`, and trace context
|
|
190
|
+
fields by default. Use case `start`, `end`, and `error` phases share the same
|
|
191
|
+
span when they run with the same request context.
|
|
192
|
+
|
|
193
|
+
## Provider instrumentation
|
|
194
|
+
|
|
195
|
+
First-party and app-level providers should use
|
|
196
|
+
`createProviderInstrumentation()` from `@beignet/core/providers` instead of
|
|
197
|
+
depending on devtools directly. The helper accepts either a ports object or an
|
|
198
|
+
instrumentation port, records through `record()`, and adds provider metadata to
|
|
199
|
+
custom events. `@beignet/devtools` implements that instrumentation port
|
|
200
|
+
when `createDevtoolsProvider()` is registered.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import {
|
|
204
|
+
createProvider,
|
|
205
|
+
createProviderInstrumentation,
|
|
206
|
+
} from "@beignet/core/providers";
|
|
207
|
+
|
|
208
|
+
export const searchProvider = createProvider({
|
|
209
|
+
name: "search",
|
|
210
|
+
setup({ ports }) {
|
|
211
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
212
|
+
providerName: "search",
|
|
213
|
+
watcher: "custom",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
ports: {
|
|
218
|
+
search: {
|
|
219
|
+
async query(text: string) {
|
|
220
|
+
const results = await runSearch(text);
|
|
221
|
+
|
|
222
|
+
instrumentation.custom({
|
|
223
|
+
name: "search.query",
|
|
224
|
+
label: "Search query",
|
|
225
|
+
summary: `${results.length} results`,
|
|
226
|
+
details: { resultCount: results.length },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return results;
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Use a built-in watcher such as `db`, `cache`, `storage`, `mail`, `auth`,
|
|
239
|
+
`audit`, `rateLimit`, or `schedules` when the provider belongs to one of those
|
|
240
|
+
categories. Use `custom` or a custom watcher name for application-specific
|
|
241
|
+
integrations.
|
|
242
|
+
|
|
243
|
+
## Audit activity
|
|
244
|
+
|
|
245
|
+
Durable audit logs should still be written through your app's `AuditLogPort`.
|
|
246
|
+
Use `createDevtoolsAuditLog()` when you also want sanitized audit activity in
|
|
247
|
+
the local devtools timeline:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { createDevtoolsAuditLog } from "@beignet/devtools";
|
|
251
|
+
|
|
252
|
+
const audit = createDevtoolsAuditLog({
|
|
253
|
+
audit: durableAudit,
|
|
254
|
+
devtools: ports.devtools,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The wrapper records the durable audit entry first, then emits a custom devtools
|
|
259
|
+
event owned by the `audit` watcher. Devtools remains a local diagnostic view;
|
|
260
|
+
it is not the durable audit store.
|
|
261
|
+
|
|
262
|
+
When an audit port is transaction-scoped, emit the devtools mirror only after
|
|
263
|
+
the transaction commits. Keeping the transaction-scoped audit port durable-only
|
|
264
|
+
is preferable to showing a local audit event for work that later rolls back.
|
|
265
|
+
|
|
266
|
+
## Manual events
|
|
267
|
+
|
|
268
|
+
Use `record()` for application-specific events. It fills `id` and `timestamp`.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
ctx.ports.devtools.record({
|
|
272
|
+
type: "custom",
|
|
273
|
+
watcher: "search",
|
|
274
|
+
name: "search.query",
|
|
275
|
+
label: "Search query",
|
|
276
|
+
summary: "24 results in 18ms",
|
|
277
|
+
details: {
|
|
278
|
+
query,
|
|
279
|
+
resultCount: 24,
|
|
280
|
+
durationMs: 18,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`log()` is still available when you already have a complete `DevtoolsEvent`.
|
|
286
|
+
|
|
287
|
+
## Endpoints
|
|
288
|
+
|
|
289
|
+
- `GET /api/devtools` serves the dashboard
|
|
290
|
+
- `GET /api/devtools/core/events` returns JSON events
|
|
291
|
+
- `GET /api/devtools/stream` returns a live Server-Sent Events stream
|
|
292
|
+
- `POST /api/devtools/clear` clears the in-memory buffer and configured store
|
|
293
|
+
|
|
294
|
+
Event list query parameters:
|
|
295
|
+
|
|
296
|
+
- `type`: `request`, `error`, `usecase`, `eventBus`, `job`, `schedule`, `provider`, or `custom`
|
|
297
|
+
- `requestId`: correlation ID
|
|
298
|
+
- `traceId`: W3C trace ID
|
|
299
|
+
- `limit`: maximum events to return, default `200`
|
|
300
|
+
|
|
301
|
+
## Configuration
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
DEVTOOLS_ENABLED=true
|
|
305
|
+
DEVTOOLS_ENABLED=false
|
|
306
|
+
DEVTOOLS_MAX_EVENTS=1000
|
|
307
|
+
DEVTOOLS_PERSIST=true
|
|
308
|
+
DEVTOOLS_PERSIST_PATH=.beignet/devtools/core/events.jsonl
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The default buffer keeps the latest 500 events. Persistence is opt-in and uses
|
|
312
|
+
`.beignet/devtools/core/events.jsonl` by default when enabled without a custom
|
|
313
|
+
path.
|
|
314
|
+
|
|
315
|
+
The provider controls whether events are recorded. The HTTP route controls
|
|
316
|
+
whether those events are exposed. Both default to development-only behavior.
|
|
317
|
+
Route handlers return `404` when `NODE_ENV === "production"` unless explicitly
|
|
318
|
+
enabled:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
|
|
322
|
+
basePath: "/api/devtools",
|
|
323
|
+
enabled: process.env.DEVTOOLS_ENABLED === "true",
|
|
324
|
+
authorize: (req: Request) =>
|
|
325
|
+
req.headers.get("x-devtools-token") === process.env.DEVTOOLS_TOKEN,
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
If `authorize` returns `false`, devtools responds with `404`. If it returns a
|
|
330
|
+
`Response`, that response is used, which lets applications return their own
|
|
331
|
+
`403` or redirect response.
|
|
332
|
+
|
|
333
|
+
## Redaction
|
|
334
|
+
|
|
335
|
+
Devtools uses the shared redaction helpers from `@beignet/core/ports` before
|
|
336
|
+
events are stored. Sensitive keys such as `authorization`, `cookie`,
|
|
337
|
+
`set-cookie`, `x-api-key`, `token`, `password`, `secret`, and `credentials` are
|
|
338
|
+
replaced with `[redacted]`.
|
|
339
|
+
|
|
340
|
+
Request hooks record request headers for debugging, but do not record request or
|
|
341
|
+
response bodies by default.
|
|
342
|
+
|
|
343
|
+
You can add a custom redactor:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
createDevtoolsHooks({
|
|
347
|
+
redact: (event) => ({
|
|
348
|
+
...event,
|
|
349
|
+
details: scrub(event.details),
|
|
350
|
+
}),
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The in-memory store also accepts a redactor for custom setups:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
const devtools = createInMemoryDevtools({
|
|
358
|
+
redact: (event) => event,
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## API
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
interface DevtoolsPort {
|
|
366
|
+
log(event: DevtoolsEvent): void;
|
|
367
|
+
record(event: DevtoolsEventInput): DevtoolsEvent;
|
|
368
|
+
subscribe(listener: DevtoolsListener): () => void;
|
|
369
|
+
getEvents(filter?: DevtoolsFilter): DevtoolsEvent[];
|
|
370
|
+
getWatchers(): DevtoolsWatcher[];
|
|
371
|
+
isWatcherEnabled(name: DevtoolsWatcherName): boolean;
|
|
372
|
+
clear(): void | Promise<void>;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
function createFileDevtoolsStore(options?: {
|
|
378
|
+
filePath?: string;
|
|
379
|
+
maxEvents?: number;
|
|
380
|
+
compactEvery?: number;
|
|
381
|
+
}): DevtoolsEventStore;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
function createProviderInstrumentation(
|
|
386
|
+
target: ProviderInstrumentationTarget,
|
|
387
|
+
options: {
|
|
388
|
+
providerName: string;
|
|
389
|
+
watcher?: string;
|
|
390
|
+
redact?: (event: ProviderInstrumentationEventInput) => ProviderInstrumentationEventInput;
|
|
391
|
+
},
|
|
392
|
+
): ProviderInstrumentation;
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
function createDevtoolsAuditLog(options: {
|
|
397
|
+
audit: AuditLogPort;
|
|
398
|
+
devtools?: DevtoolsPort;
|
|
399
|
+
emit?: boolean;
|
|
400
|
+
redact?: (entry: AuditLogEntry) => AuditLogEntry;
|
|
401
|
+
}): AuditLogPort;
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
type DevtoolsEvent =
|
|
406
|
+
| RequestEvent
|
|
407
|
+
| ErrorEvent
|
|
408
|
+
| UseCaseEvent
|
|
409
|
+
| EventBusEvent
|
|
410
|
+
| JobEvent
|
|
411
|
+
| ScheduleEvent
|
|
412
|
+
| ProviderEvent
|
|
413
|
+
| CustomDevtoolsEvent;
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
All events include `id`, `timestamp`, optional `requestId`, optional `watcher`,
|
|
417
|
+
optional `traceId`, optional `spanId`, optional `parentSpanId`, optional
|
|
418
|
+
`traceparent`, and optional redacted `details`.
|
|
419
|
+
|
|
420
|
+
`createDevtoolsHooks()` accepts:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
type DevtoolsHooksOptions<Ctx> = {
|
|
424
|
+
basePath?: string;
|
|
425
|
+
requestIdHeader?: string | false;
|
|
426
|
+
traceContextHeader?: string | false;
|
|
427
|
+
getRequestId?: (args: {
|
|
428
|
+
req: HttpRequestLike;
|
|
429
|
+
ctx?: Ctx;
|
|
430
|
+
response?: HttpResponseLike;
|
|
431
|
+
}) => string | undefined;
|
|
432
|
+
getTraceContext?: (args: {
|
|
433
|
+
req: HttpRequestLike;
|
|
434
|
+
ctx?: Ctx;
|
|
435
|
+
response?: HttpResponseLike;
|
|
436
|
+
}) => DevtoolsTraceContextInput | string | undefined;
|
|
437
|
+
redact?: DevtoolsRedactor;
|
|
438
|
+
};
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`createDevtoolsRoute()` and `handleDevtoolsRequest()` accept:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
type DevtoolsRequestOptions = {
|
|
445
|
+
basePath: string;
|
|
446
|
+
enabled?: boolean;
|
|
447
|
+
authorize?: (
|
|
448
|
+
req: Request,
|
|
449
|
+
) => boolean | Response | Promise<boolean | Response>;
|
|
450
|
+
};
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Production safety
|
|
454
|
+
|
|
455
|
+
The HTTP handlers return `404` when `NODE_ENV === "production"` by default. The
|
|
456
|
+
provider also installs a no-op devtools port in production by default so app
|
|
457
|
+
code does not need null checks.
|
|
458
|
+
|
|
459
|
+
Devtools can contain sensitive request, error, and domain data. Keep it on local
|
|
460
|
+
development routes unless you intentionally add authentication and redaction.
|
|
461
|
+
|
|
462
|
+
## License
|
|
463
|
+
|
|
464
|
+
MIT
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type DevtoolsAuthorizeResult = boolean | Response;
|
|
2
|
+
export type DevtoolsAuthorize = (req: Request) => DevtoolsAuthorizeResult | Promise<DevtoolsAuthorizeResult>;
|
|
3
|
+
export interface DevtoolsRouteAccessOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the devtools route should be exposed.
|
|
6
|
+
*
|
|
7
|
+
* @default process.env.NODE_ENV !== "production"
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Optional app-owned authorization check for exposed devtools routes.
|
|
12
|
+
*
|
|
13
|
+
* Returning `false` hides devtools with a 404. Returning a `Response` lets
|
|
14
|
+
* the app provide a custom denial response.
|
|
15
|
+
*/
|
|
16
|
+
authorize?: DevtoolsAuthorize;
|
|
17
|
+
}
|
|
18
|
+
export declare function isDevtoolsRouteEnabled(options?: DevtoolsRouteAccessOptions): boolean;
|
|
19
|
+
export declare function devtoolsNotFoundResponse(): Response;
|
|
20
|
+
export declare function authorizeDevtoolsRequest(req: Request, options?: DevtoolsRouteAccessOptions): Promise<Response | undefined>;
|
|
21
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzD,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,OAAO,KACT,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEhE,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAET;AAED,wBAAgB,wBAAwB,IAAI,QAAQ,CAEnD;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAe/B"}
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function isDevtoolsRouteEnabled(options = {}) {
|
|
2
|
+
return options.enabled ?? process.env.NODE_ENV !== "production";
|
|
3
|
+
}
|
|
4
|
+
export function devtoolsNotFoundResponse() {
|
|
5
|
+
return new Response("Not Found", { status: 404 });
|
|
6
|
+
}
|
|
7
|
+
export async function authorizeDevtoolsRequest(req, options = {}) {
|
|
8
|
+
if (!isDevtoolsRouteEnabled(options)) {
|
|
9
|
+
return devtoolsNotFoundResponse();
|
|
10
|
+
}
|
|
11
|
+
if (!options.authorize) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const result = await options.authorize(req);
|
|
15
|
+
if (result instanceof Response) {
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
return result ? undefined : devtoolsNotFoundResponse();
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAuBA,MAAM,UAAU,sBAAsB,CACpC,UAAsC,EAAE;IAExC,OAAO,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,wBAAwB;IACtC,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,GAAY,EACZ,UAAsC,EAAE;IAExC,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,EAAE,CAAC;QACrC,OAAO,wBAAwB,EAAE,CAAC;IACpC,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,MAAM,YAAY,QAAQ,EAAE,CAAC;QAC/B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,wBAAwB,EAAE,CAAC;AACzD,CAAC"}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AuditLogEntry, type AuditLogPort } from "@beignet/core/ports";
|
|
2
|
+
import type { DevtoolsPort } from "./index";
|
|
3
|
+
export interface DevtoolsAuditLogOptions {
|
|
4
|
+
audit: AuditLogPort;
|
|
5
|
+
devtools?: DevtoolsPort;
|
|
6
|
+
emit?: boolean;
|
|
7
|
+
redact?: (entry: AuditLogEntry) => AuditLogEntry;
|
|
8
|
+
}
|
|
9
|
+
export declare function createDevtoolsAuditLog(options: DevtoolsAuditLogOptions): AuditLogPort;
|
|
10
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,YAAY,EAGlB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,aAAa,CAAC;CAClD;AAoBD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,YAAY,CAkCd"}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { normalizeAuditLogEntry, redactAuditLogEntry, } from "@beignet/core/ports";
|
|
2
|
+
function prepareEntry(input, redact) {
|
|
3
|
+
const redacted = redactAuditLogEntry(normalizeAuditLogEntry(input));
|
|
4
|
+
return redact ? redact(redacted) : redacted;
|
|
5
|
+
}
|
|
6
|
+
function auditSummary(entry) {
|
|
7
|
+
const resource = entry.resource?.id
|
|
8
|
+
? `${entry.resource.type}:${entry.resource.id}`
|
|
9
|
+
: entry.resource?.type;
|
|
10
|
+
const outcome = entry.outcome === "failure" ? "failed" : "succeeded";
|
|
11
|
+
return resource
|
|
12
|
+
? `${entry.action} ${outcome} for ${resource}`
|
|
13
|
+
: `${entry.action} ${outcome}`;
|
|
14
|
+
}
|
|
15
|
+
export function createDevtoolsAuditLog(options) {
|
|
16
|
+
return {
|
|
17
|
+
async record(input) {
|
|
18
|
+
const entry = prepareEntry(input, options.redact);
|
|
19
|
+
await options.audit.record(entry);
|
|
20
|
+
if (options.emit === false || !options.devtools)
|
|
21
|
+
return;
|
|
22
|
+
try {
|
|
23
|
+
options.devtools.record({
|
|
24
|
+
type: "custom",
|
|
25
|
+
watcher: "audit",
|
|
26
|
+
name: entry.action,
|
|
27
|
+
label: "Audit",
|
|
28
|
+
summary: auditSummary(entry),
|
|
29
|
+
requestId: entry.requestId,
|
|
30
|
+
traceId: entry.traceId,
|
|
31
|
+
details: {
|
|
32
|
+
action: entry.action,
|
|
33
|
+
actor: entry.actor,
|
|
34
|
+
tenant: entry.tenant,
|
|
35
|
+
resource: entry.resource,
|
|
36
|
+
outcome: entry.outcome,
|
|
37
|
+
message: entry.message,
|
|
38
|
+
metadata: entry.metadata,
|
|
39
|
+
occurredAt: entry.occurredAt.toISOString(),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Devtools is an observer; durable audit writes must not depend on it.
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAU7B,SAAS,YAAY,CACnB,KAAyB,EACzB,MAAgD;IAEhD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;IACpE,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC9C,CAAC;AAED,SAAS,YAAY,CAAC,KAAoB;IACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,EAAE;QACjC,CAAC,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE;QAC/C,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC;IACzB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC;IACrE,OAAO,QAAQ;QACb,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,OAAO,QAAQ,QAAQ,EAAE;QAC9C,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,OAAgC;IAEhC,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,KAAK;YAChB,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YAElD,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAElC,IAAI,OAAO,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ;gBAAE,OAAO;YAExD,IAAI,CAAC;gBACH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;oBACtB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,OAAO;oBAChB,IAAI,EAAE,KAAK,CAAC,MAAM;oBAClB,KAAK,EAAE,OAAO;oBACd,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC;oBAC5B,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,OAAO,EAAE;wBACP,MAAM,EAAE,KAAK,CAAC,MAAM;wBACpB,KAAK,EAAE,KAAK,CAAC,KAAK;wBAClB,MAAM,EAAE,KAAK,CAAC,MAAM;wBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;wBACxB,OAAO,EAAE,KAAK,CAAC,OAAO;wBACtB,OAAO,EAAE,KAAK,CAAC,OAAO;wBACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;wBACxB,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,WAAW,EAAE;qBAC3C;iBACF,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,uEAAuE;YACzE,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|