@next-rsc-debug/core 0.1.3

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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @next-rsc-debug/core
2
+
3
+ Framework-independent event protocol and collector for Next RSC Debug.
4
+
5
+ ## Features
6
+
7
+ - DebugEvent types and factories
8
+ - Event IDs, timestamps, and metadata
9
+ - In-memory ring buffer (default 5000 events)
10
+ - Event subscription support
11
+ - URL sanitization utilities
12
+ - Analyzer primitives (slow request, duplicate request, error detection)
13
+ - Warning detection
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add @next-rsc-debug/core
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { createEvent, getCollector, analyzeEvents } from "@next-rsc-debug/core";
25
+
26
+ const event = createEvent({
27
+ type: "fetch:start",
28
+ metadata: { url: "/api/foo" },
29
+ });
30
+ getCollector().push(event);
31
+
32
+ const events = getCollector().list();
33
+ const { warnings } = analyzeEvents(events);
34
+ ```
35
+
36
+ ## License
37
+
38
+ MIT
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Analyzer primitives: warning detection.
3
+ *
4
+ * Warnings:
5
+ * - slow-request: duration >= threshold (default 500ms)
6
+ * - duplicate-request: normalized URL groups
7
+ * - error: error events
8
+ */
9
+ import type { DebugEvent, DebugEventType } from "./events.js";
10
+ export type WarningSeverity = "info" | "warning";
11
+ export type WarningType = "slow-request" | "duplicate-request" | "error";
12
+ export interface Warning {
13
+ id: string;
14
+ type: WarningType;
15
+ severity: WarningSeverity;
16
+ eventIds: string[];
17
+ message: string;
18
+ details?: Record<string, unknown>;
19
+ }
20
+ export interface AnalyzeOptions {
21
+ slowThreshold?: number;
22
+ }
23
+ export declare function analyzeEvents(events: DebugEvent[], options?: AnalyzeOptions): {
24
+ warnings: Warning[];
25
+ };
26
+ export declare function normalizeFetchUrl(event: DebugEvent): string | undefined;
27
+ export declare function isFetchEventType(type: DebugEventType): boolean;
28
+ export declare function isNavigationEventType(type: DebugEventType): boolean;
29
+ export declare function isRscEventType(type: DebugEventType): boolean;
30
+ export declare function isCacheEventType(type: DebugEventType): boolean;
31
+ export declare function isActionEventType(type: DebugEventType): boolean;
32
+ export declare function isErrorEventType(type: DebugEventType): boolean;
33
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG9D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,mBAAmB,GAAG,OAAO,CAAC;AAEzE,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAAE,EACpB,OAAO,GAAE,cAAmB,GAC3B;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CA0EzB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAKvE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAE9D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAEnE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAE5D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAI9D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAE/D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAE9D"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Analyzer primitives: warning detection.
3
+ *
4
+ * Warnings:
5
+ * - slow-request: duration >= threshold (default 500ms)
6
+ * - duplicate-request: normalized URL groups
7
+ * - error: error events
8
+ */
9
+ import { sanitizeUrl } from "./sanitize.js";
10
+ const DEFAULT_SLOW_THRESHOLD = 500;
11
+ export function analyzeEvents(events, options = {}) {
12
+ const slowThreshold = options.slowThreshold ?? DEFAULT_SLOW_THRESHOLD;
13
+ const warnings = [];
14
+ // Slow request detection: events with duration >= threshold.
15
+ for (const event of events) {
16
+ if (event.duration !== undefined && event.duration >= slowThreshold) {
17
+ warnings.push({
18
+ id: `warn_slow_${event.id}`,
19
+ type: "slow-request",
20
+ severity: "warning",
21
+ eventIds: [event.id],
22
+ message: `Slow operation detected: ${event.duration}ms (threshold: ${slowThreshold}ms)`,
23
+ details: {
24
+ duration: event.duration,
25
+ threshold: slowThreshold,
26
+ type: event.type,
27
+ requestId: event.requestId,
28
+ },
29
+ });
30
+ }
31
+ }
32
+ // Duplicate detection: group fetch events by normalized URL.
33
+ const fetchGroups = new Map();
34
+ for (const event of events) {
35
+ if (event.type === "fetch:end" && event.metadata?.url) {
36
+ const url = sanitizeUrl(String(event.metadata.url));
37
+ const group = fetchGroups.get(url);
38
+ if (group) {
39
+ group.push(event);
40
+ }
41
+ else {
42
+ fetchGroups.set(url, [event]);
43
+ }
44
+ }
45
+ }
46
+ for (const [url, group] of fetchGroups) {
47
+ if (group.length > 1) {
48
+ warnings.push({
49
+ id: `warn_dup_${url.replace(/[^a-zA-Z0-9]/g, "_")}`,
50
+ type: "duplicate-request",
51
+ severity: "warning",
52
+ eventIds: group.map((e) => e.id),
53
+ message: `Duplicate request detected: ${url} (${group.length} times)`,
54
+ details: {
55
+ url,
56
+ count: group.length,
57
+ requestIds: group
58
+ .map((e) => e.requestId)
59
+ .filter((id) => id !== undefined),
60
+ },
61
+ });
62
+ }
63
+ }
64
+ // Error detection.
65
+ for (const event of events) {
66
+ if (event.type === "error") {
67
+ warnings.push({
68
+ id: `warn_error_${event.id}`,
69
+ type: "error",
70
+ severity: "warning",
71
+ eventIds: [event.id],
72
+ message: String(event.metadata?.message ?? "Unknown error"),
73
+ details: {
74
+ requestId: event.requestId,
75
+ metadata: event.metadata,
76
+ },
77
+ });
78
+ }
79
+ }
80
+ return { warnings };
81
+ }
82
+ export function normalizeFetchUrl(event) {
83
+ if (event.type !== "fetch:end" || !event.metadata?.url) {
84
+ return undefined;
85
+ }
86
+ return sanitizeUrl(String(event.metadata.url));
87
+ }
88
+ export function isFetchEventType(type) {
89
+ return type === "fetch:start" || type === "fetch:end";
90
+ }
91
+ export function isNavigationEventType(type) {
92
+ return type === "navigation:start" || type === "navigation:end";
93
+ }
94
+ export function isRscEventType(type) {
95
+ return type === "rsc:start" || type === "rsc:end";
96
+ }
97
+ export function isCacheEventType(type) {
98
+ return (type === "cache:hit" || type === "cache:miss" || type === "cache:invalidate");
99
+ }
100
+ export function isActionEventType(type) {
101
+ return type === "action:start" || type === "action:end";
102
+ }
103
+ export function isErrorEventType(type) {
104
+ return type === "error";
105
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Event collector: singleton ring buffer with subscription support.
3
+ *
4
+ * This is the central store used by the Next.js integration and DevTools UI.
5
+ *
6
+ * The instance is stored on `globalThis` rather than in module scope. Next.js
7
+ * bundles each route separately on the server, so a module-level variable
8
+ * would be duplicated per route chunk: events recorded while rendering a
9
+ * Server Component would land in a different collector than the one the
10
+ * debug-events route reads. Keying off `globalThis` gives every route chunk
11
+ * a single shared collector.
12
+ */
13
+ import { type EventListener, type Unsubscribe } from "./ring-buffer.js";
14
+ import type { DebugEvent } from "./events.js";
15
+ export interface Collector {
16
+ push(event: DebugEvent): void;
17
+ list(): DebugEvent[];
18
+ listRange(start: number, end: number): DebugEvent[];
19
+ clear(): void;
20
+ size(): number;
21
+ capacity(): number;
22
+ subscribe(listener: EventListener): Unsubscribe;
23
+ destroy(): void;
24
+ }
25
+ export declare function getCollector(): Collector;
26
+ export declare function createCollector(max?: number): Collector;
27
+ export declare function setCollector(collector: Collector): void;
28
+ export declare function resetCollector(): void;
29
+ //# sourceMappingURL=collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,IAAI,IAAI,UAAU,EAAE,CAAC;IACrB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CAAC;IACpD,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,MAAM,CAAC;IACf,QAAQ,IAAI,MAAM,CAAC;IACnB,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,WAAW,CAAC;IAChD,OAAO,IAAI,IAAI,CAAC;CACjB;AAoDD,wBAAgB,YAAY,IAAI,SAAS,CAMxC;AAED,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAEvD;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAEvD;AAED,wBAAgB,cAAc,IAAI,IAAI,CAMrC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Event collector: singleton ring buffer with subscription support.
3
+ *
4
+ * This is the central store used by the Next.js integration and DevTools UI.
5
+ *
6
+ * The instance is stored on `globalThis` rather than in module scope. Next.js
7
+ * bundles each route separately on the server, so a module-level variable
8
+ * would be duplicated per route chunk: events recorded while rendering a
9
+ * Server Component would land in a different collector than the one the
10
+ * debug-events route reads. Keying off `globalThis` gives every route chunk
11
+ * a single shared collector.
12
+ */
13
+ import { RingBuffer, } from "./ring-buffer.js";
14
+ class MemoryCollector {
15
+ buffer;
16
+ constructor(max) {
17
+ this.buffer = new RingBuffer({ max });
18
+ }
19
+ push(event) {
20
+ this.buffer.push(event);
21
+ }
22
+ list() {
23
+ return this.buffer.list();
24
+ }
25
+ listRange(start, end) {
26
+ return this.buffer.listRange(start, end);
27
+ }
28
+ clear() {
29
+ this.buffer.clear();
30
+ }
31
+ size() {
32
+ return this.buffer.size;
33
+ }
34
+ capacity() {
35
+ return this.buffer.capacity;
36
+ }
37
+ subscribe(listener) {
38
+ return this.buffer.subscribe(listener);
39
+ }
40
+ destroy() {
41
+ this.buffer.destroy();
42
+ }
43
+ }
44
+ const COLLECTOR_KEY = Symbol.for("next-rsc-debug.collector");
45
+ function collectorGlobal() {
46
+ return globalThis;
47
+ }
48
+ export function getCollector() {
49
+ const store = collectorGlobal();
50
+ if (!store[COLLECTOR_KEY]) {
51
+ store[COLLECTOR_KEY] = new MemoryCollector();
52
+ }
53
+ return store[COLLECTOR_KEY];
54
+ }
55
+ export function createCollector(max) {
56
+ return new MemoryCollector(max);
57
+ }
58
+ export function setCollector(collector) {
59
+ collectorGlobal()[COLLECTOR_KEY] = collector;
60
+ }
61
+ export function resetCollector() {
62
+ const store = collectorGlobal();
63
+ if (store[COLLECTOR_KEY]) {
64
+ store[COLLECTOR_KEY].destroy();
65
+ store[COLLECTOR_KEY] = undefined;
66
+ }
67
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DebugEvent types and factories for Next RSC Debug.
3
+ *
4
+ * Every event has a unique ID, timestamp, and type.
5
+ * Optional fields: duration, requestId, parentId, metadata.
6
+ */
7
+ export type DebugEventType = "navigation:start" | "navigation:end" | "rsc:start" | "rsc:end" | "fetch:start" | "fetch:end" | "cache:hit" | "cache:miss" | "cache:invalidate" | "action:start" | "action:end" | "error";
8
+ export interface DebugEvent {
9
+ id: string;
10
+ type: DebugEventType;
11
+ timestamp: number;
12
+ duration?: number;
13
+ requestId?: string;
14
+ parentId?: string;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ export interface CreateEventOptions {
18
+ type: DebugEventType;
19
+ requestId?: string;
20
+ parentId?: string;
21
+ duration?: number;
22
+ metadata?: Record<string, unknown>;
23
+ }
24
+ export declare function createRequestId(): string;
25
+ export declare function createNavigationId(): string;
26
+ export declare function createEventId(prefix: string): string;
27
+ export declare function createEvent(options: CreateEventOptions): DebugEvent;
28
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,gBAAgB,GAChB,WAAW,GACX,SAAS,GACT,aAAa,GACb,WAAW,GACX,WAAW,GACX,YAAY,GACZ,kBAAkB,GAClB,cAAc,GACd,YAAY,GACZ,OAAO,CAAC;AAEZ,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAkBD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU,CAmBnE"}
package/dist/events.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * DebugEvent types and factories for Next RSC Debug.
3
+ *
4
+ * Every event has a unique ID, timestamp, and type.
5
+ * Optional fields: duration, requestId, parentId, metadata.
6
+ */
7
+ let idCounter = 0;
8
+ let lastTimestamp = 0;
9
+ function generateId(prefix) {
10
+ // Use a combination of timestamp, counter, and random for uniqueness.
11
+ const now = Date.now();
12
+ if (now === lastTimestamp) {
13
+ idCounter += 1;
14
+ }
15
+ else {
16
+ idCounter = 0;
17
+ lastTimestamp = now;
18
+ }
19
+ const random = Math.floor(Math.random() * 1_000_000);
20
+ return `${prefix}_${now}_${idCounter}_${random}`;
21
+ }
22
+ export function createRequestId() {
23
+ return generateId("req");
24
+ }
25
+ export function createNavigationId() {
26
+ return generateId("nav");
27
+ }
28
+ export function createEventId(prefix) {
29
+ return generateId(prefix);
30
+ }
31
+ export function createEvent(options) {
32
+ const event = {
33
+ id: generateId(options.type.replace(/:/g, "_")),
34
+ type: options.type,
35
+ timestamp: Date.now(),
36
+ };
37
+ if (options.requestId !== undefined) {
38
+ event.requestId = options.requestId;
39
+ }
40
+ if (options.parentId !== undefined) {
41
+ event.parentId = options.parentId;
42
+ }
43
+ if (options.duration !== undefined) {
44
+ event.duration = options.duration;
45
+ }
46
+ if (options.metadata !== undefined) {
47
+ event.metadata = options.metadata;
48
+ }
49
+ return event;
50
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public API for @next-rsc-debug/core.
3
+ */
4
+ export { createEvent, createEventId, createRequestId, createNavigationId, type DebugEvent, type DebugEventType, type CreateEventOptions, } from "./events.js";
5
+ export { sanitizeUrl, stripSensitiveQueryParams, isSafeKey, } from "./sanitize.js";
6
+ export { RingBuffer, type EventListener, type Unsubscribe, type RingBufferOptions, } from "./ring-buffer.js";
7
+ export { analyzeEvents, type Warning, type WarningType, type WarningSeverity, type AnalyzeOptions, normalizeFetchUrl, isFetchEventType, isNavigationEventType, isRscEventType, isCacheEventType, isActionEventType, isErrorEventType, } from "./analyzer.js";
8
+ export { getCollector, createCollector, setCollector, resetCollector, type Collector, } from "./collector.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,WAAW,EACX,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,WAAW,EACX,yBAAyB,EACzB,SAAS,GACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,UAAU,EACV,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,cAAc,EACd,KAAK,SAAS,GACf,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public API for @next-rsc-debug/core.
3
+ */
4
+ export { createEvent, createEventId, createRequestId, createNavigationId, } from "./events.js";
5
+ export { sanitizeUrl, stripSensitiveQueryParams, isSafeKey, } from "./sanitize.js";
6
+ export { RingBuffer, } from "./ring-buffer.js";
7
+ export { analyzeEvents, normalizeFetchUrl, isFetchEventType, isNavigationEventType, isRscEventType, isCacheEventType, isActionEventType, isErrorEventType, } from "./analyzer.js";
8
+ export { getCollector, createCollector, setCollector, resetCollector, } from "./collector.js";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * In-memory ring buffer for events.
3
+ *
4
+ * Requirements:
5
+ * - Configurable max event count (default 5000).
6
+ * - FIFO eviction.
7
+ * - list(), clear(), push().
8
+ * - Subscription support.
9
+ * - Safe concurrent usage in Node.js.
10
+ * - No memory leaks.
11
+ */
12
+ import type { DebugEvent } from "./events.js";
13
+ export type EventListener = (event: DebugEvent) => void;
14
+ export type Unsubscribe = () => void;
15
+ export interface RingBufferOptions {
16
+ max?: number;
17
+ }
18
+ export declare class RingBuffer {
19
+ private readonly max;
20
+ private events;
21
+ private listeners;
22
+ constructor(options?: RingBufferOptions);
23
+ push(event: DebugEvent): void;
24
+ list(): DebugEvent[];
25
+ listRange(start: number, end: number): DebugEvent[];
26
+ clear(): void;
27
+ get size(): number;
28
+ get capacity(): number;
29
+ subscribe(listener: EventListener): Unsubscribe;
30
+ get listenerCount(): number;
31
+ destroy(): void;
32
+ }
33
+ //# sourceMappingURL=ring-buffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ring-buffer.d.ts","sourceRoot":"","sources":["../src/ring-buffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AACxD,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAID,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,SAAS,CAAuB;gBAE5B,OAAO,GAAE,iBAAsB;IAO3C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAgB7B,IAAI,IAAI,UAAU,EAAE;IAIpB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE;IAOnD,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,WAAW;IAU/C,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * In-memory ring buffer for events.
3
+ *
4
+ * Requirements:
5
+ * - Configurable max event count (default 5000).
6
+ * - FIFO eviction.
7
+ * - list(), clear(), push().
8
+ * - Subscription support.
9
+ * - Safe concurrent usage in Node.js.
10
+ * - No memory leaks.
11
+ */
12
+ const DEFAULT_MAX = 5000;
13
+ export class RingBuffer {
14
+ max;
15
+ events = [];
16
+ listeners = [];
17
+ constructor(options = {}) {
18
+ this.max = options.max ?? DEFAULT_MAX;
19
+ if (!Number.isFinite(this.max) || this.max <= 0) {
20
+ throw new Error("RingBuffer max must be a positive number");
21
+ }
22
+ }
23
+ push(event) {
24
+ this.events.push(event);
25
+ if (this.events.length > this.max) {
26
+ this.events.shift();
27
+ }
28
+ // Notify listeners (synchronous, bounded).
29
+ for (let i = 0; i < this.listeners.length; i++) {
30
+ const listener = this.listeners[i];
31
+ try {
32
+ listener(event);
33
+ }
34
+ catch {
35
+ // Never let a listener crash the collector.
36
+ }
37
+ }
38
+ }
39
+ list() {
40
+ return this.events.slice();
41
+ }
42
+ listRange(start, end) {
43
+ const len = this.events.length;
44
+ const from = Math.max(0, Math.min(start, len));
45
+ const to = Math.max(0, Math.min(end, len));
46
+ return this.events.slice(from, to);
47
+ }
48
+ clear() {
49
+ this.events = [];
50
+ }
51
+ get size() {
52
+ return this.events.length;
53
+ }
54
+ get capacity() {
55
+ return this.max;
56
+ }
57
+ subscribe(listener) {
58
+ this.listeners.push(listener);
59
+ return () => {
60
+ const index = this.listeners.indexOf(listener);
61
+ if (index !== -1) {
62
+ this.listeners.splice(index, 1);
63
+ }
64
+ };
65
+ }
66
+ get listenerCount() {
67
+ return this.listeners.length;
68
+ }
69
+ destroy() {
70
+ this.events = [];
71
+ this.listeners = [];
72
+ }
73
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sanitization utilities for URLs and metadata.
3
+ *
4
+ * Rules:
5
+ * - Strip query strings by default.
6
+ * - Strip credentials embedded in URLs.
7
+ * - Never store cookies, auth headers, request bodies, response bodies,
8
+ * passwords, or tokens.
9
+ */
10
+ export declare function sanitizeUrl(url: string): string;
11
+ export declare function stripSensitiveQueryParams(url: string): string;
12
+ export declare function isSafeKey(key: string): boolean;
13
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAyB/C;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA+B7D;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAM9C"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Sanitization utilities for URLs and metadata.
3
+ *
4
+ * Rules:
5
+ * - Strip query strings by default.
6
+ * - Strip credentials embedded in URLs.
7
+ * - Never store cookies, auth headers, request bodies, response bodies,
8
+ * passwords, or tokens.
9
+ */
10
+ const SENSITIVE_QUERY_PARAMS = new Set([
11
+ "token",
12
+ "access_token",
13
+ "refresh_token",
14
+ "id_token",
15
+ "api_key",
16
+ "apikey",
17
+ "secret",
18
+ "password",
19
+ "passwd",
20
+ "pwd",
21
+ "auth",
22
+ "authorization",
23
+ "session",
24
+ "sessionid",
25
+ "cookie",
26
+ "csrf",
27
+ "xsrf",
28
+ "private_key",
29
+ "client_secret",
30
+ ]);
31
+ export function sanitizeUrl(url) {
32
+ if (typeof url !== "string" || url.length === 0) {
33
+ return "";
34
+ }
35
+ let sanitized;
36
+ try {
37
+ const parsed = new URL(url);
38
+ // Strip the query string entirely.
39
+ parsed.search = "";
40
+ // Strip the hash fragment.
41
+ parsed.hash = "";
42
+ sanitized = parsed.toString();
43
+ }
44
+ catch {
45
+ // If the URL cannot be parsed, strip anything that looks like a query string.
46
+ sanitized = url.split("?")[0].split("#")[0];
47
+ }
48
+ // Also strip credentials embedded in the URL (user:pass@host).
49
+ sanitized = sanitized.replace(/^(https?:\/\/)[^:/@\s]+:[^@/\s]+@/, "$1");
50
+ // Remove userinfo entirely if present (user@host without password).
51
+ sanitized = sanitized.replace(/^(https?:\/\/)[^@/\s]+@/, "$1");
52
+ return sanitized;
53
+ }
54
+ export function stripSensitiveQueryParams(url) {
55
+ if (typeof url !== "string" || url.length === 0) {
56
+ return "";
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = new URL(url);
61
+ }
62
+ catch {
63
+ return url.split("#")[0];
64
+ }
65
+ const params = parsed.searchParams;
66
+ const sensitiveKeys = [];
67
+ for (const key of params.keys()) {
68
+ if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) {
69
+ sensitiveKeys.push(key);
70
+ }
71
+ }
72
+ if (sensitiveKeys.length === 0) {
73
+ return url.split("#")[0];
74
+ }
75
+ for (const key of sensitiveKeys) {
76
+ params.delete(key);
77
+ }
78
+ parsed.search = params.toString();
79
+ parsed.hash = "";
80
+ return parsed.toString();
81
+ }
82
+ export function isSafeKey(key) {
83
+ if (typeof key !== "string") {
84
+ return false;
85
+ }
86
+ const lower = key.toLowerCase();
87
+ return !SENSITIVE_QUERY_PARAMS.has(lower);
88
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@next-rsc-debug/core",
3
+ "version": "0.1.3",
4
+ "description": "Framework-independent event protocol and collector for Next RSC Debug",
5
+ "author": "Samith Mendis",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "typecheck": "tsc --noEmit -p tsconfig.json",
22
+ "test": "vitest run --config vitest.config.ts",
23
+ "lint": "eslint src --ext .ts"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.0.0",
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^3.2.4"
32
+ }
33
+ }