@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { DevtoolsEvent } from "./events";
|
|
2
|
+
|
|
3
|
+
export interface DevtoolsEventStore {
|
|
4
|
+
name?: string;
|
|
5
|
+
load?(): DevtoolsEvent[] | Promise<DevtoolsEvent[]>;
|
|
6
|
+
append?(
|
|
7
|
+
event: DevtoolsEvent,
|
|
8
|
+
events: readonly DevtoolsEvent[],
|
|
9
|
+
): void | Promise<void>;
|
|
10
|
+
clear?(): void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FileDevtoolsStoreOptions {
|
|
14
|
+
/**
|
|
15
|
+
* JSONL file used for persisted devtools events.
|
|
16
|
+
*
|
|
17
|
+
* @default ".beignet/devtools/core/events.jsonl"
|
|
18
|
+
*/
|
|
19
|
+
filePath?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of events to load and keep when compacting the file.
|
|
23
|
+
*
|
|
24
|
+
* @default 1000
|
|
25
|
+
*/
|
|
26
|
+
maxEvents?: number;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite the JSONL file after this many appended events. Compaction keeps
|
|
30
|
+
* persistence bounded without making every record pay the rewrite cost.
|
|
31
|
+
*
|
|
32
|
+
* @default 50
|
|
33
|
+
*/
|
|
34
|
+
compactEvery?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_FILE_PATH = ".beignet/devtools/core/events.jsonl";
|
|
38
|
+
const DEFAULT_MAX_EVENTS = 1000;
|
|
39
|
+
const DEFAULT_COMPACT_EVERY = 50;
|
|
40
|
+
|
|
41
|
+
type NodeFs = typeof import("node:fs/promises");
|
|
42
|
+
type NodePath = typeof import("node:path");
|
|
43
|
+
|
|
44
|
+
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|
45
|
+
return (
|
|
46
|
+
typeof error === "object" &&
|
|
47
|
+
error !== null &&
|
|
48
|
+
"code" in error &&
|
|
49
|
+
typeof (error as { code?: unknown }).code === "string"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isDevtoolsEvent(value: unknown): value is DevtoolsEvent {
|
|
54
|
+
return (
|
|
55
|
+
typeof value === "object" &&
|
|
56
|
+
value !== null &&
|
|
57
|
+
"id" in value &&
|
|
58
|
+
"timestamp" in value &&
|
|
59
|
+
"type" in value
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseJsonLine(line: string): DevtoolsEvent | undefined {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(line);
|
|
66
|
+
return isDevtoolsEvent(parsed) ? parsed : undefined;
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadNodeModules(): Promise<{
|
|
73
|
+
fs: NodeFs;
|
|
74
|
+
path: NodePath;
|
|
75
|
+
}> {
|
|
76
|
+
const [fs, path] = await Promise.all([
|
|
77
|
+
import("node:fs/promises"),
|
|
78
|
+
import("node:path"),
|
|
79
|
+
]);
|
|
80
|
+
return { fs, path };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createFileDevtoolsStore(
|
|
84
|
+
options: FileDevtoolsStoreOptions = {},
|
|
85
|
+
): DevtoolsEventStore {
|
|
86
|
+
const filePath = options.filePath ?? DEFAULT_FILE_PATH;
|
|
87
|
+
const maxEvents = Math.max(1, options.maxEvents ?? DEFAULT_MAX_EVENTS);
|
|
88
|
+
const compactEvery = Math.max(
|
|
89
|
+
1,
|
|
90
|
+
options.compactEvery ?? DEFAULT_COMPACT_EVERY,
|
|
91
|
+
);
|
|
92
|
+
let appendCount = 0;
|
|
93
|
+
let queue = Promise.resolve();
|
|
94
|
+
|
|
95
|
+
async function ensureDirectory(fs: NodeFs, path: NodePath): Promise<void> {
|
|
96
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readEvents(): Promise<DevtoolsEvent[]> {
|
|
100
|
+
const { fs } = await loadNodeModules();
|
|
101
|
+
let content: string;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
content = await fs.readFile(filePath, "utf8");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (isErrnoException(error) && error.code === "ENOENT") return [];
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return content
|
|
111
|
+
.split("\n")
|
|
112
|
+
.map((line) => line.trim())
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.map(parseJsonLine)
|
|
115
|
+
.filter((event): event is DevtoolsEvent => event !== undefined)
|
|
116
|
+
.slice(-maxEvents);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function writeEvents(events: readonly DevtoolsEvent[]): Promise<void> {
|
|
120
|
+
const { fs, path } = await loadNodeModules();
|
|
121
|
+
await ensureDirectory(fs, path);
|
|
122
|
+
const content = events.map((event) => JSON.stringify(event)).join("\n");
|
|
123
|
+
await fs.writeFile(filePath, content ? `${content}\n` : "", "utf8");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function enqueue(task: () => Promise<void>): Promise<void> {
|
|
127
|
+
queue = queue.then(task, task);
|
|
128
|
+
return queue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
name: "file",
|
|
133
|
+
async load() {
|
|
134
|
+
const events = await readEvents();
|
|
135
|
+
await writeEvents(events);
|
|
136
|
+
return events;
|
|
137
|
+
},
|
|
138
|
+
append(event, events) {
|
|
139
|
+
appendCount += 1;
|
|
140
|
+
const currentAppendCount = appendCount;
|
|
141
|
+
|
|
142
|
+
return enqueue(async () => {
|
|
143
|
+
const shouldCompact =
|
|
144
|
+
currentAppendCount % compactEvery === 0 || events.length >= maxEvents;
|
|
145
|
+
|
|
146
|
+
if (shouldCompact) {
|
|
147
|
+
await writeEvents(events.slice(-maxEvents));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { fs, path } = await loadNodeModules();
|
|
152
|
+
await ensureDirectory(fs, path);
|
|
153
|
+
await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
clear() {
|
|
157
|
+
appendCount = 0;
|
|
158
|
+
return enqueue(async () => {
|
|
159
|
+
await writeEvents([]);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createProviderInstrumentation,
|
|
3
|
+
isProviderInstrumentationPort,
|
|
4
|
+
type ProviderCustomInstrumentationEventInput,
|
|
5
|
+
type ProviderInstrumentation,
|
|
6
|
+
type ProviderInstrumentationOptions,
|
|
7
|
+
} from "@beignet/core/providers";
|
|
8
|
+
import type { DevtoolsPort } from "./index";
|
|
9
|
+
|
|
10
|
+
export type ProviderDevtoolsOptions = ProviderInstrumentationOptions;
|
|
11
|
+
export type ProviderCustomDevtoolsEventInput =
|
|
12
|
+
ProviderCustomInstrumentationEventInput;
|
|
13
|
+
export type ProviderDevtools = ProviderInstrumentation;
|
|
14
|
+
|
|
15
|
+
export function isDevtoolsPort(value: unknown): value is DevtoolsPort {
|
|
16
|
+
return (
|
|
17
|
+
isProviderInstrumentationPort(value) &&
|
|
18
|
+
typeof value === "object" &&
|
|
19
|
+
value !== null &&
|
|
20
|
+
"getEvents" in value &&
|
|
21
|
+
typeof value.getEvents === "function" &&
|
|
22
|
+
"isWatcherEnabled" in value &&
|
|
23
|
+
typeof value.isWatcherEnabled === "function"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveDevtoolsPort(target: unknown): DevtoolsPort | undefined {
|
|
28
|
+
if (isDevtoolsPort(target)) return target;
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
typeof target === "object" &&
|
|
32
|
+
target !== null &&
|
|
33
|
+
"devtools" in target &&
|
|
34
|
+
isDevtoolsPort(target.devtools)
|
|
35
|
+
) {
|
|
36
|
+
return target.devtools;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createProviderDevtools(
|
|
43
|
+
target: unknown,
|
|
44
|
+
options: ProviderDevtoolsOptions,
|
|
45
|
+
): ProviderDevtools {
|
|
46
|
+
return createProviderInstrumentation(
|
|
47
|
+
resolveDevtoolsPort(target) ?? undefined,
|
|
48
|
+
options,
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devtools provider implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides an in-memory event buffer that can be attached to ctx.ports.devtools.
|
|
5
|
+
* The provider is disabled by default in production environments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createProvider } from "@beignet/core/providers";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import type {
|
|
11
|
+
DevtoolsEvent,
|
|
12
|
+
DevtoolsEventInput,
|
|
13
|
+
DevtoolsListener,
|
|
14
|
+
DevtoolsRedactor,
|
|
15
|
+
DevtoolsSubscriptionEvent,
|
|
16
|
+
} from "./events";
|
|
17
|
+
import type { DevtoolsFilter, DevtoolsPort } from "./index";
|
|
18
|
+
import {
|
|
19
|
+
createFileDevtoolsStore,
|
|
20
|
+
type DevtoolsEventStore,
|
|
21
|
+
} from "./persistence";
|
|
22
|
+
import {
|
|
23
|
+
applyDevtoolsRedaction,
|
|
24
|
+
createRedactionFailureEvent,
|
|
25
|
+
} from "./redaction";
|
|
26
|
+
import {
|
|
27
|
+
type DevtoolsWatcher,
|
|
28
|
+
type DevtoolsWatchersOptions,
|
|
29
|
+
isDevtoolsEventEnabled,
|
|
30
|
+
isDevtoolsWatcherEnabled,
|
|
31
|
+
resolveDevtoolsWatchers,
|
|
32
|
+
} from "./watchers";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of events to keep in the buffer.
|
|
36
|
+
* When this limit is reached, the oldest events are removed.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_EVENTS = 500;
|
|
39
|
+
|
|
40
|
+
export interface InMemoryDevtoolsOptions {
|
|
41
|
+
maxEvents?: number;
|
|
42
|
+
redact?: DevtoolsRedactor;
|
|
43
|
+
watchers?: DevtoolsWatchersOptions;
|
|
44
|
+
initialEvents?: readonly DevtoolsEvent[];
|
|
45
|
+
store?: DevtoolsEventStore;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createEventId(): string {
|
|
49
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
50
|
+
return crypto.randomUUID();
|
|
51
|
+
}
|
|
52
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createDevtoolsEvent(event: DevtoolsEventInput): DevtoolsEvent {
|
|
56
|
+
return {
|
|
57
|
+
...event,
|
|
58
|
+
id: event.id ?? createEventId(),
|
|
59
|
+
timestamp: event.timestamp ?? new Date().toISOString(),
|
|
60
|
+
} as DevtoolsEvent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create an in-memory devtools implementation.
|
|
65
|
+
*
|
|
66
|
+
* This implementation stores events in a bounded buffer (max 500 events).
|
|
67
|
+
* When the buffer is full, the oldest events are removed to make room for new ones.
|
|
68
|
+
*
|
|
69
|
+
* @returns A DevtoolsPort implementation
|
|
70
|
+
*/
|
|
71
|
+
export function createInMemoryDevtools(
|
|
72
|
+
options: InMemoryDevtoolsOptions = {},
|
|
73
|
+
): DevtoolsPort {
|
|
74
|
+
const listeners = new Set<DevtoolsListener>();
|
|
75
|
+
const maxEvents = options.maxEvents ?? MAX_EVENTS;
|
|
76
|
+
const watchers = resolveDevtoolsWatchers(options.watchers);
|
|
77
|
+
const events: DevtoolsEvent[] = [];
|
|
78
|
+
|
|
79
|
+
function notify(event: DevtoolsSubscriptionEvent): void {
|
|
80
|
+
for (const listener of listeners) {
|
|
81
|
+
try {
|
|
82
|
+
listener(event);
|
|
83
|
+
} catch {
|
|
84
|
+
// Subscribers are observers; they must not affect devtools storage.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getErrorMessage(error: unknown): string {
|
|
90
|
+
if (error instanceof Error) return error.message;
|
|
91
|
+
if (typeof error === "string") return error;
|
|
92
|
+
return "Unknown error";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function persistEvent(event: DevtoolsEvent): void {
|
|
96
|
+
if (!options.store?.append) return;
|
|
97
|
+
|
|
98
|
+
void Promise.resolve(options.store.append(event, [...events])).catch(
|
|
99
|
+
(error) => {
|
|
100
|
+
storeInternalError({
|
|
101
|
+
type: "error",
|
|
102
|
+
message: "Devtools persistence failed",
|
|
103
|
+
details: {
|
|
104
|
+
message: getErrorMessage(error),
|
|
105
|
+
store: options.store?.name,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function clearPersistedEvents(): Promise<void> {
|
|
113
|
+
if (!options.store?.clear) return;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await options.store.clear();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
storeInternalError({
|
|
119
|
+
type: "error",
|
|
120
|
+
message: "Devtools persistence clear failed",
|
|
121
|
+
details: {
|
|
122
|
+
message: getErrorMessage(error),
|
|
123
|
+
store: options.store?.name,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function store(
|
|
130
|
+
event: DevtoolsEvent,
|
|
131
|
+
storeOptions: { notify?: boolean; persist?: boolean } = {},
|
|
132
|
+
): DevtoolsEvent {
|
|
133
|
+
events.push(event);
|
|
134
|
+
if (events.length > maxEvents) {
|
|
135
|
+
events.splice(0, events.length - maxEvents);
|
|
136
|
+
}
|
|
137
|
+
if (storeOptions.notify !== false) {
|
|
138
|
+
notify({ type: "record", event });
|
|
139
|
+
}
|
|
140
|
+
if (storeOptions.persist !== false) {
|
|
141
|
+
persistEvent(event);
|
|
142
|
+
}
|
|
143
|
+
return event;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function redact(event: DevtoolsEvent): DevtoolsEvent {
|
|
147
|
+
try {
|
|
148
|
+
return applyDevtoolsRedaction(event, options.redact);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return createRedactionFailureEvent(error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function storeInternalError(event: DevtoolsEventInput): DevtoolsEvent {
|
|
155
|
+
const normalized = createDevtoolsEvent(event);
|
|
156
|
+
const redacted = redact(normalized);
|
|
157
|
+
if (!isDevtoolsEventEnabled(watchers, redacted)) return redacted;
|
|
158
|
+
return store(redacted, { persist: false });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const event of options.initialEvents ?? []) {
|
|
162
|
+
const redacted = redact(event);
|
|
163
|
+
if (isDevtoolsEventEnabled(watchers, redacted)) {
|
|
164
|
+
store(redacted, { notify: false, persist: false });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function log(event: DevtoolsEvent): void {
|
|
169
|
+
const redacted = redact(event);
|
|
170
|
+
if (!isDevtoolsEventEnabled(watchers, redacted)) return;
|
|
171
|
+
store(redacted);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function record(event: DevtoolsEventInput): DevtoolsEvent {
|
|
175
|
+
const normalized = createDevtoolsEvent(event);
|
|
176
|
+
const redacted = redact(normalized);
|
|
177
|
+
if (!isDevtoolsEventEnabled(watchers, redacted)) return redacted;
|
|
178
|
+
return store(redacted);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getEvents(filter?: DevtoolsFilter): DevtoolsEvent[] {
|
|
182
|
+
let result = [...events];
|
|
183
|
+
|
|
184
|
+
if (filter?.type) {
|
|
185
|
+
result = result.filter((e) => e.type === filter.type);
|
|
186
|
+
}
|
|
187
|
+
if (filter?.requestId) {
|
|
188
|
+
result = result.filter((e) => e.requestId === filter.requestId);
|
|
189
|
+
}
|
|
190
|
+
if (filter?.traceId) {
|
|
191
|
+
result = result.filter((e) => e.traceId === filter.traceId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const limit = filter?.limit ?? 200;
|
|
195
|
+
if (result.length > limit) {
|
|
196
|
+
result = result.slice(result.length - limit);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function clear(): Promise<void> {
|
|
203
|
+
events.length = 0;
|
|
204
|
+
notify({ type: "clear" });
|
|
205
|
+
await clearPersistedEvents();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getWatchers(): DevtoolsWatcher[] {
|
|
209
|
+
return watchers.map((watcher) => ({ ...watcher }));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isWatcherEnabled(name: string): boolean {
|
|
213
|
+
return isDevtoolsWatcherEnabled(watchers, name);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function subscribe(listener: DevtoolsListener): () => void {
|
|
217
|
+
listeners.add(listener);
|
|
218
|
+
return () => {
|
|
219
|
+
listeners.delete(listener);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
log,
|
|
225
|
+
record,
|
|
226
|
+
subscribe,
|
|
227
|
+
getEvents,
|
|
228
|
+
clear,
|
|
229
|
+
getWatchers,
|
|
230
|
+
isWatcherEnabled,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Configuration schema for the devtools provider.
|
|
236
|
+
*/
|
|
237
|
+
const DevtoolsConfigSchema = z.object({
|
|
238
|
+
ENABLED: z
|
|
239
|
+
.string()
|
|
240
|
+
.optional()
|
|
241
|
+
.transform((v) => (v === undefined ? undefined : v === "true")),
|
|
242
|
+
MAX_EVENTS: z.coerce.number().int().positive().optional(),
|
|
243
|
+
PERSIST: z
|
|
244
|
+
.string()
|
|
245
|
+
.optional()
|
|
246
|
+
.transform((v) => (v === undefined ? undefined : v === "true")),
|
|
247
|
+
PERSIST_PATH: z.string().optional(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
export type DevtoolsConfig = z.infer<typeof DevtoolsConfigSchema>;
|
|
251
|
+
|
|
252
|
+
export interface DevtoolsProviderOptions {
|
|
253
|
+
enabled?: boolean;
|
|
254
|
+
maxEvents?: number;
|
|
255
|
+
redact?: DevtoolsRedactor;
|
|
256
|
+
watchers?: DevtoolsWatchersOptions;
|
|
257
|
+
store?:
|
|
258
|
+
| DevtoolsEventStore
|
|
259
|
+
| (() => DevtoolsEventStore | Promise<DevtoolsEventStore>);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Devtools service provider.
|
|
264
|
+
*
|
|
265
|
+
* This provider attaches a DevtoolsPort to ctx.ports.devtools.
|
|
266
|
+
* It can be enabled/disabled via the DEVTOOLS_ENABLED environment variable.
|
|
267
|
+
* By default, it's enabled in non-production environments and disabled in production.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* import { createDevtoolsProvider } from "@beignet/devtools";
|
|
272
|
+
* import { createServer } from "@beignet/core/server";
|
|
273
|
+
*
|
|
274
|
+
* const server = await createServer({
|
|
275
|
+
* ports,
|
|
276
|
+
* providers: [createDevtoolsProvider()],
|
|
277
|
+
* });
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* To explicitly enable/disable:
|
|
281
|
+
* ```bash
|
|
282
|
+
* DEVTOOLS_ENABLED=true npm run dev
|
|
283
|
+
* DEVTOOLS_ENABLED=false npm run dev
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
export function createDevtoolsProvider(options: DevtoolsProviderOptions = {}) {
|
|
287
|
+
return createProvider<
|
|
288
|
+
Record<string, unknown>,
|
|
289
|
+
typeof DevtoolsConfigSchema,
|
|
290
|
+
{ devtools: DevtoolsPort }
|
|
291
|
+
>({
|
|
292
|
+
name: "devtools",
|
|
293
|
+
|
|
294
|
+
config: {
|
|
295
|
+
schema: DevtoolsConfigSchema,
|
|
296
|
+
envPrefix: "DEVTOOLS_",
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async setup({ config }) {
|
|
300
|
+
const enabled =
|
|
301
|
+
options.enabled ??
|
|
302
|
+
config?.ENABLED ??
|
|
303
|
+
process.env.NODE_ENV !== "production";
|
|
304
|
+
|
|
305
|
+
if (!enabled) {
|
|
306
|
+
const watchers = resolveDevtoolsWatchers(options.watchers).map(
|
|
307
|
+
(watcher) => ({
|
|
308
|
+
...watcher,
|
|
309
|
+
enabled: false,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Provide a no-op implementation so callers don't need null checks
|
|
314
|
+
const noop: DevtoolsPort = {
|
|
315
|
+
log: () => {},
|
|
316
|
+
record: (event) => createDevtoolsEvent(event),
|
|
317
|
+
subscribe: () => () => {},
|
|
318
|
+
getEvents: () => [],
|
|
319
|
+
clear: () => {},
|
|
320
|
+
getWatchers: () => watchers.map((watcher) => ({ ...watcher })),
|
|
321
|
+
isWatcherEnabled: () => false,
|
|
322
|
+
};
|
|
323
|
+
return { ports: { devtools: noop } };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const maxEvents = config?.MAX_EVENTS ?? options.maxEvents ?? MAX_EVENTS;
|
|
327
|
+
const configuredStore =
|
|
328
|
+
typeof options.store === "function"
|
|
329
|
+
? await options.store()
|
|
330
|
+
: options.store;
|
|
331
|
+
let store =
|
|
332
|
+
configuredStore ??
|
|
333
|
+
(config?.PERSIST || config?.PERSIST_PATH
|
|
334
|
+
? createFileDevtoolsStore({
|
|
335
|
+
filePath: config.PERSIST_PATH,
|
|
336
|
+
maxEvents,
|
|
337
|
+
})
|
|
338
|
+
: undefined);
|
|
339
|
+
let initialEvents: DevtoolsEvent[] = [];
|
|
340
|
+
let loadError: unknown;
|
|
341
|
+
|
|
342
|
+
if (store?.load) {
|
|
343
|
+
try {
|
|
344
|
+
initialEvents = await store.load();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
loadError = error;
|
|
347
|
+
store = undefined;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const devtools = createInMemoryDevtools({
|
|
352
|
+
maxEvents,
|
|
353
|
+
redact: options.redact,
|
|
354
|
+
watchers: options.watchers,
|
|
355
|
+
initialEvents,
|
|
356
|
+
store,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (loadError) {
|
|
360
|
+
devtools.record({
|
|
361
|
+
type: "error",
|
|
362
|
+
message: "Devtools persistence load failed",
|
|
363
|
+
details: {
|
|
364
|
+
message:
|
|
365
|
+
loadError instanceof Error
|
|
366
|
+
? loadError.message
|
|
367
|
+
: String(loadError),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { ports: { devtools } };
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
package/src/redaction.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { redactValue } from "@beignet/core/ports";
|
|
2
|
+
import type { DevtoolsEvent, DevtoolsRedactor } from "./events";
|
|
3
|
+
|
|
4
|
+
export const defaultDevtoolsRedactor: DevtoolsRedactor = (event) =>
|
|
5
|
+
redactValue(event);
|
|
6
|
+
|
|
7
|
+
export function applyDevtoolsRedaction(
|
|
8
|
+
event: DevtoolsEvent,
|
|
9
|
+
redact?: DevtoolsRedactor,
|
|
10
|
+
): DevtoolsEvent {
|
|
11
|
+
const redacted = defaultDevtoolsRedactor(event);
|
|
12
|
+
if (!redact) return redacted;
|
|
13
|
+
return redact(redacted);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createRedactionFailureEvent(error: unknown): DevtoolsEvent {
|
|
17
|
+
return {
|
|
18
|
+
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
type: "error",
|
|
21
|
+
message: "Devtools redactor failed",
|
|
22
|
+
details: {
|
|
23
|
+
message: error instanceof Error ? error.message : String(error),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|