@angelitosystems/devtools-protocol 1.0.8 → 1.0.10
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.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/events.ts","../src/redact.ts","../src/editor-urls.ts"],"sourcesContent":["/**\n * @angelitosystems/devtools-protocol\n *\n * Typed WebSocket protocol shared by the NestJS DevTools SDK, server and dashboard.\n * Zero runtime dependencies — safe to embed in every tier.\n */\nexport * from './types';\nexport * from './events';\nexport * from './redact';\nexport * from './editor-urls';\n","/** Wire protocol version. Bump on breaking payload changes. */\nexport const PROTOCOL_VERSION = 1 as const;\nexport const MIN_SUPPORTED_PROTOCOL_VERSION = 1 as const;\n\n/** JSON-RPC-ish envelope shared by every message on the wire. */\nexport interface DevToolsMessage<T = unknown> {\n /** Envelope format version. */\n v: typeof PROTOCOL_VERSION;\n /** Unique message id. */\n id: string;\n /** Originating project (absent for client->server handshake-less control traffic). */\n projectId?: string;\n /** Milliseconds since epoch. */\n ts: number;\n /** Event discriminator. */\n event: DevToolsEventName;\n /** Event payload. */\n payload: T;\n}\n\n/** All event names supported by protocol v1. */\nexport type DevToolsEventName =\n // lifecycle\n | 'project.connected'\n | 'project.disconnected'\n // http\n | 'request.started'\n | 'request.completed'\n // observability\n | 'log.created'\n | 'error.created'\n | 'query.executed'\n // realtime\n | 'websocket.connected'\n | 'websocket.message'\n // perf\n | 'performance.updated'\n | 'profile.started'\n | 'profile.completed'\n | 'plugin.event'\n // application graph\n | 'app.snapshot'\n // control plane\n | 'client.hello'\n | 'client.welcome'\n | 'stream.pause'\n | 'stream.resume'\n | 'state.clear'\n | 'state.snapshot'\n | 'state.ack'\n | 'error';\n\n/** Union of every typed payload keyed by its event name. */\nexport interface DevToolsEventMap {\n 'project.connected': ProjectInfo;\n 'project.disconnected': { projectId: string; reason?: string };\n 'request.started': RequestStartedPayload;\n 'request.completed': RequestCompletedPayload;\n 'log.created': LogPayload;\n 'error.created': ErrorPayload;\n 'query.executed': QueryPayload;\n 'websocket.connected': GatewayConnectionPayload;\n 'websocket.message': GatewayMessagePayload;\n 'performance.updated': PerformanceSnapshot;\n 'profile.started': ProfileStartedPayload;\n 'profile.completed': ProfileCompletedPayload;\n 'plugin.event': PluginEventPayload;\n 'app.snapshot': AppSnapshot;\n 'client.hello': ClientHello;\n 'client.welcome': { serverVersion: string; protocol: typeof PROTOCOL_VERSION };\n 'stream.pause': Record<string, never>;\n 'stream.resume': Record<string, never>;\n 'state.clear': { scope: 'logs' | 'requests' | 'errors' | 'queries' | 'all' };\n 'state.snapshot': StateSnapshot;\n 'state.ack': { ok: true };\n error: { code: string; message: string };\n}\n\n/** Discriminated union of all wire messages. */\nexport type DevToolsEvent = {\n [K in DevToolsEventName]: DevToolsMessage<DevToolsEventMap[K]>;\n}[DevToolsEventName];\n\n/** Metadata every NestJS application reports when it connects. */\nexport interface ProjectInfo {\n projectId: string;\n projectName: string;\n environment: string;\n hostname: string;\n port: number | null;\n pid: number;\n runtime: string;\n runtimeVersion: string;\n nodeVersion: string;\n nestjsVersion: string | null;\n sdkVersion: string;\n}\n\n/** One timeline entry of a request (guard, interceptor, service call...). */\nexport interface TimelineSpan {\n /** Logical layer, e.g. 'middleware' | 'guard' | 'interceptor' | 'pipe' | 'controller' | 'service' | 'database' | 'response'. */\n layer: string;\n /** Human label, e.g. 'JwtAuthGuard' or 'SELECT users'. */\n label: string;\n /** ms */\n duration: number;\n startedAt: number;\n status?: 'ok' | 'error';\n detail?: string;\n /** Source location when available (file, line, column, function). */\n source?: SourceLocation;\n}\n\n/** Emitted the moment a request enters the SDK. */\nexport interface RequestStartedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n httpVersion?: string;\n headers: Record<string, string>;\n query: Record<string, unknown>;\n params?: Record<string, unknown>;\n ip?: string;\n userAgent?: string;\n startedAt: number;\n}\n\n/** Emitted when the response finishes. */\nexport interface RequestCompletedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n statusCode: number;\n duration: number;\n startedAt: number;\n timeline: TimelineSpan[];\n query?: Record<string, unknown>;\n headers?: Record<string, string>;\n responsePreview?: string;\n responseBody?: unknown;\n requestBody?: unknown;\n errored: boolean;\n}\n\n/** A captured console or logger entry. */\nexport interface LogPayload {\n requestId?: string;\n projectId: string;\n level: LogLevel;\n message: string;\n arguments?: unknown[];\n stack?: string;\n source?: SourceLocation;\n context?: string;\n processId: number;\n timestamp: number;\n}\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'verbose';\n\n/** File/line/column triple resolved from source maps when available. */\nexport interface SourceLocation {\n file: string;\n line: number;\n column: number;\n /** Absolute path when resolvable on the host machine. */\n absolutePath?: string;\n /** function name if the stack exposed one */\n function?: string;\n}\n\n/** A captured exception with source mapping. */\nexport interface ErrorPayload {\n requestId?: string;\n projectId: string;\n name: string;\n message: string;\n stack?: string;\n source?: SourceLocation;\n /** stable hash for grouping identical errors */\n fingerprint: string;\n context?: string;\n request?: { method: string; url: string; statusCode?: number };\n controller?: string;\n service?: string;\n timestamp: number;\n /** server-side occurrence count for grouped errors */\n occurrences?: number;\n}\n\n/** A captured database query. */\nexport interface QueryPayload {\n requestId?: string;\n projectId: string;\n provider: 'prisma' | 'typeorm' | 'sequelize' | 'mikroorm' | 'other';\n sql: string;\n duration: number;\n database?: string;\n parameters?: unknown[];\n timestamp: number;\n}\n\n/** Gateway-level connection snapshot. */\nexport interface GatewayConnectionPayload {\n projectId: string;\n gateway: string;\n namespace: string;\n connections: number;\n timestamp: number;\n}\n\n/** Individual gateway event flow. */\nexport interface GatewayMessagePayload {\n projectId: string;\n gateway: string;\n event: string;\n direction: 'received' | 'sent';\n payloadSize: number;\n error?: string;\n duration?: number;\n requestId?: string;\n timestamp: number;\n}\n\n/** Point-in-time process metrics. */\nexport interface PerformanceSnapshot {\n projectId: string;\n timestamp: number;\n cpuPercent: number;\n memoryUsedBytes: number;\n memoryTotalBytes: number;\n heapUsedBytes: number;\n heapTotalBytes: number;\n eventLoopLagMs: number;\n activeRequests: number;\n requestsPerSecond: number;\n averageLatencyMs: number;\n p95LatencyMs: number;\n p99LatencyMs: number;\n errorsPerSecond: number;\n /** true when the process reports memory pressure */\n memoryPressure?: boolean;\n}\n\n/** A profiling session requested by a developer or extension. */\nexport interface ProfileStartedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n durationMs?: number;\n}\n\n/** Result metadata for a completed profiling session. */\nexport interface ProfileCompletedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n completedAt: number;\n data?: unknown;\n}\n\n/** Namespaced event emitted by a registered plugin. */\nexport interface PluginEventPayload {\n projectId: string;\n plugin: string;\n name: string;\n data?: unknown;\n timestamp: number;\n}\n\n/** Static description of the NestJS application graph. */\nexport interface AppSnapshot {\n projectId: string;\n projectName: string;\n modules: AppModuleNode[];\n nestjsVersion: string | null;\n capturedAt: number;\n}\n\n/** A module and its members in the application graph. */\nexport interface AppModuleNode {\n name: string;\n imports: string[];\n controllers: AppMemberNode[];\n providers: AppMemberNode[];\n exports: string[];\n}\n\n/** A member (controller/provider/guard/pipe/...) inside a module. */\nexport interface AppMemberNode {\n name: string;\n type: 'controller' | 'provider' | 'guard' | 'interceptor' | 'pipe' | 'filter' | 'gateway';\n routes?: string[];\n}\n\n/** What a dashboard/control client announces when it connects. */\nexport interface ClientHello {\n kind: 'dashboard' | 'cli' | 'other';\n name?: string;\n version?: string;\n}\n\n/** Full server state handed to newly connected dashboards. */\nexport interface StateSnapshot {\n projects: ProjectInfo[];\n requests: RequestCompletedPayload[];\n logs: LogPayload[];\n errors: ErrorPayload[];\n queries: QueryPayload[];\n websocketConnections: GatewayConnectionPayload[];\n websocketMessages: GatewayMessagePayload[];\n performance: Record<string, PerformanceSnapshot>;\n apps: Record<string, AppSnapshot>;\n}\n","import type { DevToolsEventMap, DevToolsEventName, DevToolsMessage } from './types';\nimport { MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION } from './types';\n\n/** Const array of every event name (runtime mirror of the type union). */\nexport const DEVTOOLS_EVENTS = [\n 'project.connected',\n 'project.disconnected',\n 'request.started',\n 'request.completed',\n 'log.created',\n 'error.created',\n 'query.executed',\n 'websocket.connected',\n 'websocket.message',\n 'performance.updated',\n 'profile.started',\n 'profile.completed',\n 'plugin.event',\n 'app.snapshot',\n 'client.hello',\n 'client.welcome',\n 'stream.pause',\n 'stream.resume',\n 'state.clear',\n 'state.snapshot',\n 'state.ack',\n 'error',\n] as const satisfies readonly DevToolsEventName[];\n\n/** True when `name` is a known protocol event. */\nexport function isDevToolsEventName(name: string): name is DevToolsEventName {\n return (DEVTOOLS_EVENTS as readonly string[]).includes(name);\n}\n\n/** Build a fully-typed wire message. */\nexport function createMessage<K extends DevToolsEventName>(\n event: K,\n payload: DevToolsEventMap[K],\n options?: { projectId?: string; id?: string; ts?: number },\n): DevToolsMessage<DevToolsEventMap[K]> {\n return {\n v: PROTOCOL_VERSION,\n id: options?.id ?? randomId(),\n projectId: options?.projectId,\n ts: options?.ts ?? Date.now(),\n event,\n payload,\n };\n}\n\n/** Cheap collision-resistant id (no crypto dependency needed for wire correlation). */\nexport function randomId(prefix?: string): string {\n const core =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID().replace(/-/g, '').slice(0, 16)\n : Math.random().toString(36).slice(2, 10) + Date.now().toString(36);\n return prefix ? `${prefix}_${core}` : core;\n}\n\n/** Type guard narrowing an unknown parsed frame into a DevToolsMessage. */\nexport function parseMessage(raw: string): DevToolsMessage | null {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'event' in parsed &&\n 'payload' in parsed &&\n 'v' in parsed &&\n Number((parsed as { v: unknown }).v) === PROTOCOL_VERSION &&\n typeof (parsed as { id?: unknown }).id === 'string' &&\n typeof (parsed as { ts?: unknown }).ts === 'number' &&\n isDevToolsEventName(String((parsed as { event: unknown }).event))\n ) {\n return parsed as DevToolsMessage;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** True when a peer can safely communicate with this package. */\nexport function isSupportedProtocolVersion(version: number): boolean {\n return Number.isInteger(version) && version >= MIN_SUPPORTED_PROTOCOL_VERSION && version <= PROTOCOL_VERSION;\n}\n","/** Default keys that are always redacted unless explicitly allowed. */\nexport const DEFAULT_REDACT_KEYS = [\n 'password',\n 'token',\n 'access_token',\n 'accessToken',\n 'refresh_token',\n 'refreshToken',\n 'authorization',\n 'cookie',\n 'cookies',\n 'secret',\n 'apiKey',\n 'api_key',\n 'client_secret',\n 'clientSecret',\n 'private_key',\n 'privateKey',\n 'session',\n 'set-cookie',\n] as const;\n\n/** Default capture ceilings shared by SDK and server. */\nexport const MAX_CAPTURE_BYTES = 4 * 1024;\nexport const MAX_DEPTH = 4;\n\n/** Options for deep redaction of arbitrary values. */\nexport interface RedactOptions {\n /** Extra key names to redact (case-insensitive, partial match allowed). */\n redact?: string[];\n /** Keys that should never be redacted even if they look sensitive. */\n allow?: string[];\n /** Placeholder string used for redacted values. */\n placeholder?: string;\n /** Max serialized size before truncation. */\n maxBytes?: number;\n /** Max object depth. */\n maxDepth?: number;\n}\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]';\nconst TRUNCATION_SUFFIX = '…[truncated]';\n\n/** Normalized set of denylist and allowlist key matchers. */\nexport class Redactor {\n private readonly deny: Set<string>;\n private readonly allow: Set<string>;\n private readonly placeholder: string;\n private readonly maxBytes: number;\n private readonly maxDepth: number;\n\n constructor(options?: RedactOptions) {\n this.deny = new Set([...DEFAULT_REDACT_KEYS, ...(options?.redact ?? [])].map(normalizeKey));\n this.allow = new Set((options?.allow ?? []).map(normalizeKey));\n this.placeholder = options?.placeholder ?? DEFAULT_PLACEHOLDER;\n this.maxBytes = options?.maxBytes ?? MAX_CAPTURE_BYTES;\n this.maxDepth = options?.maxDepth ?? MAX_DEPTH;\n }\n\n /** True when the given key is sensitive and not allowed. */\n isSensitive(key: string): boolean {\n const normalized = normalizeKey(key);\n if (this.allow.has(normalized)) return false;\n if (this.deny.has(normalized)) return true;\n // partial match: `userPassword`, `authTokenValue`, `x-api-key`...\n for (const denied of this.deny) {\n if (normalized.includes(denied)) return true;\n }\n return false;\n }\n\n /**\n * Deep-copy a value while redacting sensitive keys, truncating oversized\n * strings and enforcing depth limits. Circular references become '[Circular]'.\n */\n redact(value: unknown, depth = 0, seen = new Set<unknown>()): unknown {\n if (value === null || typeof value !== 'object') {\n return redactPrimitive(value);\n }\n if (seen.has(value)) return '[Circular]';\n if (depth >= this.maxDepth) return '[MaxDepth]';\n\n seen.add(value);\n try {\n if (Array.isArray(value)) {\n return value.slice(0, 50).map((item) => this.redact(item, depth + 1, seen));\n }\n if (value instanceof Error) {\n return { name: value.name, message: value.message };\n }\n if (value instanceof Date) return value.toISOString();\n\n const out: Record<string, unknown> = {};\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n out[key] = this.isSensitive(key) ? this.placeholder : this.redact(raw, depth + 1, seen);\n }\n return out;\n } finally {\n seen.delete(value);\n }\n }\n\n /** Redact sensitive substrings (key=value / key: value / bearer tokens) from free-form text. */\n redactString(text: string): string {\n let out = text;\n for (const denied of this.deny) {\n const camel = denied;\n const pattern = new RegExp(`(${escapeRegExp(camel)}|${escapeRegExp(denied.replace(/[-_]/g, ''))})\\\\s*[=:]\\\\s*([^\\\\s,&;]+)`, 'gi');\n out = out.replace(pattern, `$1=${this.placeholder}`);\n }\n out = out.replace(/bearer\\s+[a-z0-9._-]+/gi, `Bearer ${this.placeholder}`);\n return out;\n }\n\n /** Serialize a value safely: redacted, size-capped, never throws. */\n serialize(value: unknown): string {\n try {\n const safe = this.redact(value);\n const json = JSON.stringify(safe) ?? String(safe);\n return truncateUtf8(json, this.maxBytes, TRUNCATION_SUFFIX);\n } catch {\n return '[Unserializable]';\n }\n }\n}\n\n/** Shared default redactor instance for quick helpers. */\nexport const defaultRedactor = new Redactor();\n\n/** Convenience: deep-redact with the default policy. */\nexport function redactValue(value: unknown): unknown {\n return defaultRedactor.redact(value);\n}\n\n/** Convenience: scrub a free-form string with the default policy. */\nexport function redactText(text: string): string {\n return defaultRedactor.redactString(text);\n}\n\nfunction redactPrimitive(value: unknown): unknown {\n if (typeof value === 'string') return defaultRedactor.redactString(value);\n if (typeof value === 'number' || typeof value === 'boolean' || value === null) return value;\n if (typeof value === 'function') return '[Function]';\n if (typeof value === 'bigint') return value.toString() + 'n';\n return String(value);\n}\n\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[-_\\s]/g, '');\n}\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** UTF-8-safe truncation that never splits a multi-byte character. */\nfunction truncateUtf8(text: string, maxBytes: number, suffix: string): string {\n const buf = Buffer.from(text, 'utf8');\n if (buf.length <= maxBytes) return text;\n const cut = Math.max(0, maxBytes - Buffer.byteLength(suffix, 'utf8'));\n return buf.subarray(0, cut).toString('utf8') + suffix;\n}\n","/**\n * Deep links that open a file at a specific line/column in an editor.\n * Paths are generated for the OS the DevTools *server* runs on — never assume\n * Windows or POSIX layout; detect it at runtime.\n */\n\nexport type EditorKind = 'vscode' | 'cursor' | 'zed' | 'sublime';\n\nconst EDITOR_SCHEMES: Record<EditorKind, string> = {\n vscode: 'vscode',\n cursor: 'cursor',\n zed: 'zed',\n sublime: 'subl',\n};\n\n/** Returns 'win32' | 'darwin' | 'linux' | 'unknown' without touching process when unavailable. */\nexport function detectPlatform(platformGetter?: () => string): 'win32' | 'darwin' | 'linux' | 'unknown' {\n try {\n const platform = platformGetter ? platformGetter() : process.platform;\n if (platform === 'win32' || platform === 'darwin' || platform === 'linux') return platform;\n return 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\nfunction encodeFilePath(absolutePath: string, platform: string): string {\n // VS Code expects forward slashes; keep drive letters like /C:/... on Windows.\n const normalized = platform === 'win32' ? absolutePath.replace(/\\\\/g, '/') : absolutePath;\n const withDrive = platform === 'win32' && /^[A-Za-z]:\\//.test(normalized) ? `/${normalized}` : normalized;\n // the scheme already carries 'file/', so drop any leading slash;\n // preserve the drive-letter colon (C:) which VS Code expects unencoded\n return withDrive\n .replace(/^\\//, '')\n .split('/')\n .map((segment, index) => (index === 0 && /^[A-Za-z]:$/.test(segment) ? segment : encodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * Build an editor deep link such as:\n * vscode://file/Users/me/app/src/users.service.ts:87:21\n */\nexport function buildEditorUrl(\n editor: EditorKind,\n file: { absolutePath?: string; file?: string; line?: number; column?: number },\n platformGetter?: () => string,\n): string | null {\n const absolutePath = file.absolutePath ?? file.file;\n if (!absolutePath) return null;\n\n const platform = detectPlatform(platformGetter);\n const scheme = EDITOR_SCHEMES[editor];\n const path = encodeFilePath(absolutePath, platform);\n const line = file.line ?? 1;\n const column = file.column ?? 1;\n\n if (editor === 'sublime') {\n return `subl://open?url=file://${path}&line=${line}&column=${column}`;\n }\n return `${scheme}://file/${path}:${line}:${column}`;\n}\n\n/** First available vscode:// link for a source location. */\nexport function vscodeUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('vscode', file);\n}\n\n/** First available cursor:// link for a source location. */\nexport function cursorUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('cursor', file);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/events.ts","../src/redact.ts","../src/editor-urls.ts"],"sourcesContent":["/**\n * @angelitosystems/devtools-protocol\n *\n * Typed WebSocket protocol shared by the NestJS DevTools SDK, server and dashboard.\n * Zero runtime dependencies — safe to embed in every tier.\n */\nexport * from './types';\nexport * from './events';\nexport * from './redact';\nexport * from './editor-urls';\n","/** Wire protocol version. Bump on breaking payload changes. */\nexport const PROTOCOL_VERSION = 1 as const;\nexport const MIN_SUPPORTED_PROTOCOL_VERSION = 1 as const;\n\n/** JSON-RPC-ish envelope shared by every message on the wire. */\nexport interface DevToolsMessage<T = unknown> {\n /** Envelope format version. */\n v: typeof PROTOCOL_VERSION;\n /** Unique message id. */\n id: string;\n /** Originating project (absent for client->server handshake-less control traffic). */\n projectId?: string;\n /** Milliseconds since epoch. */\n ts: number;\n /** Event discriminator. */\n event: DevToolsEventName;\n /** Event payload. */\n payload: T;\n}\n\n/** All event names supported by protocol v1. */\nexport type DevToolsEventName =\n // lifecycle\n | 'project.connected'\n | 'project.disconnected'\n // http\n | 'request.started'\n | 'request.completed'\n // observability\n | 'log.created'\n | 'error.created'\n | 'query.executed'\n // realtime\n | 'websocket.connected'\n | 'websocket.message'\n // perf\n | 'performance.updated'\n | 'profile.started'\n | 'profile.completed'\n | 'plugin.event'\n | 'compatibility.warning'\n // application graph\n | 'app.snapshot'\n // control plane\n | 'client.hello'\n | 'client.welcome'\n | 'stream.pause'\n | 'stream.resume'\n | 'state.clear'\n | 'state.snapshot'\n | 'state.ack'\n | 'error';\n\n/** Union of every typed payload keyed by its event name. */\nexport interface DevToolsEventMap {\n 'project.connected': ProjectInfo;\n 'project.disconnected': { projectId: string; reason?: string };\n 'request.started': RequestStartedPayload;\n 'request.completed': RequestCompletedPayload;\n 'log.created': LogPayload;\n 'error.created': ErrorPayload;\n 'query.executed': QueryPayload;\n 'websocket.connected': GatewayConnectionPayload;\n 'websocket.message': GatewayMessagePayload;\n 'performance.updated': PerformanceSnapshot;\n 'profile.started': ProfileStartedPayload;\n 'profile.completed': ProfileCompletedPayload;\n 'plugin.event': PluginEventPayload;\n 'compatibility.warning': CompatibilityWarningPayload;\n 'app.snapshot': AppSnapshot;\n 'client.hello': ClientHello;\n 'client.welcome': { serverVersion: string; protocol: typeof PROTOCOL_VERSION };\n 'stream.pause': Record<string, never>;\n 'stream.resume': Record<string, never>;\n 'state.clear': { scope: 'logs' | 'requests' | 'errors' | 'queries' | 'all' };\n 'state.snapshot': StateSnapshot;\n 'state.ack': { ok: true };\n error: { code: string; message: string };\n}\n\n/** Discriminated union of all wire messages. */\nexport type DevToolsEvent = {\n [K in DevToolsEventName]: DevToolsMessage<DevToolsEventMap[K]>;\n}[DevToolsEventName];\n\n/** Metadata every NestJS application reports when it connects. */\nexport interface ProjectInfo {\n projectId: string;\n projectName: string;\n environment: string;\n hostname: string;\n port: number | null;\n pid: number;\n runtime: string;\n runtimeVersion: string;\n nodeVersion: string;\n nestjsVersion: string | null;\n sdkVersion: string;\n}\n\n/** One timeline entry of a request (guard, interceptor, service call...). */\nexport interface TimelineSpan {\n /** Logical layer, e.g. 'middleware' | 'guard' | 'interceptor' | 'pipe' | 'controller' | 'service' | 'database' | 'response'. */\n layer: string;\n /** Human label, e.g. 'JwtAuthGuard' or 'SELECT users'. */\n label: string;\n /** ms */\n duration: number;\n startedAt: number;\n status?: 'ok' | 'error';\n detail?: string;\n /** Source location when available (file, line, column, function). */\n source?: SourceLocation;\n}\n\n/** Emitted the moment a request enters the SDK. */\nexport interface RequestStartedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n httpVersion?: string;\n headers: Record<string, string>;\n query: Record<string, unknown>;\n params?: Record<string, unknown>;\n ip?: string;\n userAgent?: string;\n startedAt: number;\n}\n\n/** Emitted when the response finishes. */\nexport interface RequestCompletedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n statusCode: number;\n duration: number;\n startedAt: number;\n timeline: TimelineSpan[];\n requestHeaders?: Record<string, string>;\n query?: Record<string, unknown>;\n headers?: Record<string, string>;\n responsePreview?: string;\n responseBody?: unknown;\n requestBody?: unknown;\n errored: boolean;\n}\n\n/** A captured console or logger entry. */\nexport interface LogPayload {\n requestId?: string;\n projectId: string;\n level: LogLevel;\n message: string;\n arguments?: unknown[];\n stack?: string;\n source?: SourceLocation;\n context?: string;\n processId: number;\n timestamp: number;\n}\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'verbose';\n\n/** File/line/column triple resolved from source maps when available. */\nexport interface SourceLocation {\n file: string;\n line: number;\n column: number;\n /** Absolute path when resolvable on the host machine. */\n absolutePath?: string;\n /** function name if the stack exposed one */\n function?: string;\n}\n\n/** A captured exception with source mapping. */\nexport interface ErrorPayload {\n requestId?: string;\n projectId: string;\n name: string;\n message: string;\n stack?: string;\n source?: SourceLocation;\n /** stable hash for grouping identical errors */\n fingerprint: string;\n context?: string;\n request?: { method: string; url: string; statusCode?: number };\n controller?: string;\n service?: string;\n timestamp: number;\n /** server-side occurrence count for grouped errors */\n occurrences?: number;\n}\n\n/** A captured database query. */\nexport interface QueryPayload {\n requestId?: string;\n projectId: string;\n provider: 'prisma' | 'typeorm' | 'sequelize' | 'mikroorm' | 'other';\n sql: string;\n duration: number;\n database?: string;\n parameters?: unknown[];\n timestamp: number;\n}\n\n/** Gateway-level connection snapshot. */\nexport interface GatewayConnectionPayload {\n projectId: string;\n gateway: string;\n namespace: string;\n connections: number;\n timestamp: number;\n}\n\n/** Individual gateway event flow. */\nexport interface GatewayMessagePayload {\n projectId: string;\n gateway: string;\n event: string;\n direction: 'received' | 'sent';\n payloadSize: number;\n payloadPreview?: unknown;\n error?: string;\n duration?: number;\n requestId?: string;\n timestamp: number;\n}\n\n/** Point-in-time process metrics. */\nexport interface PerformanceSnapshot {\n projectId: string;\n timestamp: number;\n cpuPercent: number;\n memoryUsedBytes: number;\n memoryTotalBytes: number;\n heapUsedBytes: number;\n heapTotalBytes: number;\n eventLoopLagMs: number;\n activeRequests: number;\n requestsPerSecond: number;\n averageLatencyMs: number;\n p95LatencyMs: number;\n p99LatencyMs: number;\n errorsPerSecond: number;\n /** true when the process reports memory pressure */\n memoryPressure?: boolean;\n}\n\n/** A profiling session requested by a developer or extension. */\nexport interface ProfileStartedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n durationMs?: number;\n}\n\n/** Result metadata for a completed profiling session. */\nexport interface ProfileCompletedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n completedAt: number;\n data?: unknown;\n}\n\n/** Namespaced event emitted by a registered plugin. */\nexport interface PluginEventPayload {\n projectId: string;\n plugin: string;\n name: string;\n data?: unknown;\n timestamp: number;\n}\n\n/** Non-blocking version mismatch reported by the local server. */\nexport interface CompatibilityWarningPayload {\n projectId: string;\n component: 'sdk' | 'cli' | 'protocol';\n currentVersion: string;\n requiredVersion: string;\n message: string;\n updateCommand: string;\n timestamp: number;\n}\n\n/** Static description of the NestJS application graph. */\nexport interface AppSnapshot {\n projectId: string;\n projectName: string;\n modules: AppModuleNode[];\n nestjsVersion: string | null;\n capturedAt: number;\n}\n\n/** A module and its members in the application graph. */\nexport interface AppModuleNode {\n name: string;\n imports: string[];\n controllers: AppMemberNode[];\n providers: AppMemberNode[];\n exports: string[];\n}\n\n/** A member (controller/provider/guard/pipe/...) inside a module. */\nexport interface AppMemberNode {\n name: string;\n type: 'controller' | 'provider' | 'guard' | 'interceptor' | 'pipe' | 'filter' | 'gateway';\n routes?: string[];\n}\n\n/** What a dashboard/control client announces when it connects. */\nexport interface ClientHello {\n kind: 'dashboard' | 'cli' | 'other';\n name?: string;\n version?: string;\n}\n\n/** Full server state handed to newly connected dashboards. */\nexport interface StateSnapshot {\n projects: ProjectInfo[];\n requests: RequestCompletedPayload[];\n logs: LogPayload[];\n errors: ErrorPayload[];\n queries: QueryPayload[];\n websocketConnections: GatewayConnectionPayload[];\n websocketMessages: GatewayMessagePayload[];\n performance: Record<string, PerformanceSnapshot>;\n apps: Record<string, AppSnapshot>;\n}\n","import type { DevToolsEventMap, DevToolsEventName, DevToolsMessage } from './types';\nimport { MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION } from './types';\n\n/** Const array of every event name (runtime mirror of the type union). */\nexport const DEVTOOLS_EVENTS = [\n 'project.connected',\n 'project.disconnected',\n 'request.started',\n 'request.completed',\n 'log.created',\n 'error.created',\n 'query.executed',\n 'websocket.connected',\n 'websocket.message',\n 'performance.updated',\n 'profile.started',\n 'profile.completed',\n 'plugin.event',\n 'compatibility.warning',\n 'app.snapshot',\n 'client.hello',\n 'client.welcome',\n 'stream.pause',\n 'stream.resume',\n 'state.clear',\n 'state.snapshot',\n 'state.ack',\n 'error',\n] as const satisfies readonly DevToolsEventName[];\n\n/** True when `name` is a known protocol event. */\nexport function isDevToolsEventName(name: string): name is DevToolsEventName {\n return (DEVTOOLS_EVENTS as readonly string[]).includes(name);\n}\n\n/** Build a fully-typed wire message. */\nexport function createMessage<K extends DevToolsEventName>(\n event: K,\n payload: DevToolsEventMap[K],\n options?: { projectId?: string; id?: string; ts?: number },\n): DevToolsMessage<DevToolsEventMap[K]> {\n return {\n v: PROTOCOL_VERSION,\n id: options?.id ?? randomId(),\n projectId: options?.projectId,\n ts: options?.ts ?? Date.now(),\n event,\n payload,\n };\n}\n\n/** Cheap collision-resistant id (no crypto dependency needed for wire correlation). */\nexport function randomId(prefix?: string): string {\n const core =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID().replace(/-/g, '').slice(0, 16)\n : Math.random().toString(36).slice(2, 10) + Date.now().toString(36);\n return prefix ? `${prefix}_${core}` : core;\n}\n\n/** Type guard narrowing an unknown parsed frame into a DevToolsMessage. */\nexport function parseMessage(raw: string): DevToolsMessage | null {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'event' in parsed &&\n 'payload' in parsed &&\n 'v' in parsed &&\n Number((parsed as { v: unknown }).v) === PROTOCOL_VERSION &&\n typeof (parsed as { id?: unknown }).id === 'string' &&\n typeof (parsed as { ts?: unknown }).ts === 'number' &&\n isDevToolsEventName(String((parsed as { event: unknown }).event))\n ) {\n return parsed as DevToolsMessage;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** True when a peer can safely communicate with this package. */\nexport function isSupportedProtocolVersion(version: number): boolean {\n return Number.isInteger(version) && version >= MIN_SUPPORTED_PROTOCOL_VERSION && version <= PROTOCOL_VERSION;\n}\n","/** Default keys that are always redacted unless explicitly allowed. */\nexport const DEFAULT_REDACT_KEYS = [\n 'password',\n 'token',\n 'access_token',\n 'accessToken',\n 'refresh_token',\n 'refreshToken',\n 'authorization',\n 'cookie',\n 'cookies',\n 'secret',\n 'apiKey',\n 'api_key',\n 'client_secret',\n 'clientSecret',\n 'private_key',\n 'privateKey',\n 'session',\n 'set-cookie',\n] as const;\n\n/** Default capture ceilings shared by SDK and server. */\nexport const MAX_CAPTURE_BYTES = 4 * 1024;\nexport const MAX_DEPTH = 4;\n\n/** Options for deep redaction of arbitrary values. */\nexport interface RedactOptions {\n /** Extra key names to redact (case-insensitive, partial match allowed). */\n redact?: string[];\n /** Keys that should never be redacted even if they look sensitive. */\n allow?: string[];\n /** Placeholder string used for redacted values. */\n placeholder?: string;\n /** Max serialized size before truncation. */\n maxBytes?: number;\n /** Max object depth. */\n maxDepth?: number;\n}\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]';\nconst TRUNCATION_SUFFIX = '…[truncated]';\n\n/** Normalized set of denylist and allowlist key matchers. */\nexport class Redactor {\n private readonly deny: Set<string>;\n private readonly allow: Set<string>;\n private readonly placeholder: string;\n private readonly maxBytes: number;\n private readonly maxDepth: number;\n\n constructor(options?: RedactOptions) {\n this.deny = new Set([...DEFAULT_REDACT_KEYS, ...(options?.redact ?? [])].map(normalizeKey));\n this.allow = new Set((options?.allow ?? []).map(normalizeKey));\n this.placeholder = options?.placeholder ?? DEFAULT_PLACEHOLDER;\n this.maxBytes = options?.maxBytes ?? MAX_CAPTURE_BYTES;\n this.maxDepth = options?.maxDepth ?? MAX_DEPTH;\n }\n\n /** True when the given key is sensitive and not allowed. */\n isSensitive(key: string): boolean {\n const normalized = normalizeKey(key);\n if (this.allow.has(normalized)) return false;\n if (this.deny.has(normalized)) return true;\n // partial match: `userPassword`, `authTokenValue`, `x-api-key`...\n for (const denied of this.deny) {\n if (normalized.includes(denied)) return true;\n }\n return false;\n }\n\n /**\n * Deep-copy a value while redacting sensitive keys, truncating oversized\n * strings and enforcing depth limits. Circular references become '[Circular]'.\n */\n redact(value: unknown, depth = 0, seen = new Set<unknown>()): unknown {\n if (value === null || typeof value !== 'object') {\n return redactPrimitive(value);\n }\n if (seen.has(value)) return '[Circular]';\n if (depth >= this.maxDepth) return '[MaxDepth]';\n\n seen.add(value);\n try {\n if (Array.isArray(value)) {\n return value.slice(0, 50).map((item) => this.redact(item, depth + 1, seen));\n }\n if (value instanceof Error) {\n return { name: value.name, message: value.message };\n }\n if (value instanceof Date) return value.toISOString();\n\n const out: Record<string, unknown> = {};\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n out[key] = this.isSensitive(key) ? this.placeholder : this.redact(raw, depth + 1, seen);\n }\n return out;\n } finally {\n seen.delete(value);\n }\n }\n\n /** Redact sensitive substrings (key=value / key: value / bearer tokens) from free-form text. */\n redactString(text: string): string {\n let out = text;\n for (const denied of this.deny) {\n const camel = denied;\n const pattern = new RegExp(`(${escapeRegExp(camel)}|${escapeRegExp(denied.replace(/[-_]/g, ''))})\\\\s*[=:]\\\\s*([^\\\\s,&;]+)`, 'gi');\n out = out.replace(pattern, `$1=${this.placeholder}`);\n }\n out = out.replace(/bearer\\s+[a-z0-9._-]+/gi, `Bearer ${this.placeholder}`);\n return out;\n }\n\n /** Serialize a value safely: redacted, size-capped, never throws. */\n serialize(value: unknown): string {\n try {\n const safe = this.redact(value);\n const json = JSON.stringify(safe) ?? String(safe);\n return truncateUtf8(json, this.maxBytes, TRUNCATION_SUFFIX);\n } catch {\n return '[Unserializable]';\n }\n }\n}\n\n/** Shared default redactor instance for quick helpers. */\nexport const defaultRedactor = new Redactor();\n\n/** Convenience: deep-redact with the default policy. */\nexport function redactValue(value: unknown): unknown {\n return defaultRedactor.redact(value);\n}\n\n/** Convenience: scrub a free-form string with the default policy. */\nexport function redactText(text: string): string {\n return defaultRedactor.redactString(text);\n}\n\nfunction redactPrimitive(value: unknown): unknown {\n if (typeof value === 'string') return defaultRedactor.redactString(value);\n if (typeof value === 'number' || typeof value === 'boolean' || value === null) return value;\n if (typeof value === 'function') return '[Function]';\n if (typeof value === 'bigint') return value.toString() + 'n';\n return String(value);\n}\n\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[-_\\s]/g, '');\n}\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** UTF-8-safe truncation that never splits a multi-byte character. */\nfunction truncateUtf8(text: string, maxBytes: number, suffix: string): string {\n const buf = Buffer.from(text, 'utf8');\n if (buf.length <= maxBytes) return text;\n const cut = Math.max(0, maxBytes - Buffer.byteLength(suffix, 'utf8'));\n return buf.subarray(0, cut).toString('utf8') + suffix;\n}\n","/**\n * Deep links that open a file at a specific line/column in an editor.\n * Paths are generated for the OS the DevTools *server* runs on — never assume\n * Windows or POSIX layout; detect it at runtime.\n */\n\nexport type EditorKind = 'vscode' | 'cursor' | 'zed' | 'sublime';\n\nconst EDITOR_SCHEMES: Record<EditorKind, string> = {\n vscode: 'vscode',\n cursor: 'cursor',\n zed: 'zed',\n sublime: 'subl',\n};\n\n/** Returns 'win32' | 'darwin' | 'linux' | 'unknown' without touching process when unavailable. */\nexport function detectPlatform(platformGetter?: () => string): 'win32' | 'darwin' | 'linux' | 'unknown' {\n try {\n const platform = platformGetter ? platformGetter() : process.platform;\n if (platform === 'win32' || platform === 'darwin' || platform === 'linux') return platform;\n return 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\nfunction encodeFilePath(absolutePath: string, platform: string): string {\n // VS Code expects forward slashes; keep drive letters like /C:/... on Windows.\n const normalized = platform === 'win32' ? absolutePath.replace(/\\\\/g, '/') : absolutePath;\n const withDrive = platform === 'win32' && /^[A-Za-z]:\\//.test(normalized) ? `/${normalized}` : normalized;\n // the scheme already carries 'file/', so drop any leading slash;\n // preserve the drive-letter colon (C:) which VS Code expects unencoded\n return withDrive\n .replace(/^\\//, '')\n .split('/')\n .map((segment, index) => (index === 0 && /^[A-Za-z]:$/.test(segment) ? segment : encodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * Build an editor deep link such as:\n * vscode://file/Users/me/app/src/users.service.ts:87:21\n */\nexport function buildEditorUrl(\n editor: EditorKind,\n file: { absolutePath?: string; file?: string; line?: number; column?: number },\n platformGetter?: () => string,\n): string | null {\n const absolutePath = file.absolutePath ?? file.file;\n if (!absolutePath) return null;\n\n const platform = detectPlatform(platformGetter);\n const scheme = EDITOR_SCHEMES[editor];\n const path = encodeFilePath(absolutePath, platform);\n const line = file.line ?? 1;\n const column = file.column ?? 1;\n\n if (editor === 'sublime') {\n return `subl://open?url=file://${path}&line=${line}&column=${column}`;\n }\n return `${scheme}://file/${path}:${line}:${column}`;\n}\n\n/** First available vscode:// link for a source location. */\nexport function vscodeUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('vscode', file);\n}\n\n/** First available cursor:// link for a source location. */\nexport function cursorUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('cursor', file);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -17,7 +17,7 @@ interface DevToolsMessage<T = unknown> {
|
|
|
17
17
|
payload: T;
|
|
18
18
|
}
|
|
19
19
|
/** All event names supported by protocol v1. */
|
|
20
|
-
type DevToolsEventName = 'project.connected' | 'project.disconnected' | 'request.started' | 'request.completed' | 'log.created' | 'error.created' | 'query.executed' | 'websocket.connected' | 'websocket.message' | 'performance.updated' | 'profile.started' | 'profile.completed' | 'plugin.event' | 'app.snapshot' | 'client.hello' | 'client.welcome' | 'stream.pause' | 'stream.resume' | 'state.clear' | 'state.snapshot' | 'state.ack' | 'error';
|
|
20
|
+
type DevToolsEventName = 'project.connected' | 'project.disconnected' | 'request.started' | 'request.completed' | 'log.created' | 'error.created' | 'query.executed' | 'websocket.connected' | 'websocket.message' | 'performance.updated' | 'profile.started' | 'profile.completed' | 'plugin.event' | 'compatibility.warning' | 'app.snapshot' | 'client.hello' | 'client.welcome' | 'stream.pause' | 'stream.resume' | 'state.clear' | 'state.snapshot' | 'state.ack' | 'error';
|
|
21
21
|
/** Union of every typed payload keyed by its event name. */
|
|
22
22
|
interface DevToolsEventMap {
|
|
23
23
|
'project.connected': ProjectInfo;
|
|
@@ -36,6 +36,7 @@ interface DevToolsEventMap {
|
|
|
36
36
|
'profile.started': ProfileStartedPayload;
|
|
37
37
|
'profile.completed': ProfileCompletedPayload;
|
|
38
38
|
'plugin.event': PluginEventPayload;
|
|
39
|
+
'compatibility.warning': CompatibilityWarningPayload;
|
|
39
40
|
'app.snapshot': AppSnapshot;
|
|
40
41
|
'client.hello': ClientHello;
|
|
41
42
|
'client.welcome': {
|
|
@@ -114,6 +115,7 @@ interface RequestCompletedPayload {
|
|
|
114
115
|
duration: number;
|
|
115
116
|
startedAt: number;
|
|
116
117
|
timeline: TimelineSpan[];
|
|
118
|
+
requestHeaders?: Record<string, string>;
|
|
117
119
|
query?: Record<string, unknown>;
|
|
118
120
|
headers?: Record<string, string>;
|
|
119
121
|
responsePreview?: string;
|
|
@@ -193,6 +195,7 @@ interface GatewayMessagePayload {
|
|
|
193
195
|
event: string;
|
|
194
196
|
direction: 'received' | 'sent';
|
|
195
197
|
payloadSize: number;
|
|
198
|
+
payloadPreview?: unknown;
|
|
196
199
|
error?: string;
|
|
197
200
|
duration?: number;
|
|
198
201
|
requestId?: string;
|
|
@@ -242,6 +245,16 @@ interface PluginEventPayload {
|
|
|
242
245
|
data?: unknown;
|
|
243
246
|
timestamp: number;
|
|
244
247
|
}
|
|
248
|
+
/** Non-blocking version mismatch reported by the local server. */
|
|
249
|
+
interface CompatibilityWarningPayload {
|
|
250
|
+
projectId: string;
|
|
251
|
+
component: 'sdk' | 'cli' | 'protocol';
|
|
252
|
+
currentVersion: string;
|
|
253
|
+
requiredVersion: string;
|
|
254
|
+
message: string;
|
|
255
|
+
updateCommand: string;
|
|
256
|
+
timestamp: number;
|
|
257
|
+
}
|
|
245
258
|
/** Static description of the NestJS application graph. */
|
|
246
259
|
interface AppSnapshot {
|
|
247
260
|
projectId: string;
|
|
@@ -284,7 +297,7 @@ interface StateSnapshot {
|
|
|
284
297
|
}
|
|
285
298
|
|
|
286
299
|
/** Const array of every event name (runtime mirror of the type union). */
|
|
287
|
-
declare const DEVTOOLS_EVENTS: readonly ["project.connected", "project.disconnected", "request.started", "request.completed", "log.created", "error.created", "query.executed", "websocket.connected", "websocket.message", "performance.updated", "profile.started", "profile.completed", "plugin.event", "app.snapshot", "client.hello", "client.welcome", "stream.pause", "stream.resume", "state.clear", "state.snapshot", "state.ack", "error"];
|
|
300
|
+
declare const DEVTOOLS_EVENTS: readonly ["project.connected", "project.disconnected", "request.started", "request.completed", "log.created", "error.created", "query.executed", "websocket.connected", "websocket.message", "performance.updated", "profile.started", "profile.completed", "plugin.event", "compatibility.warning", "app.snapshot", "client.hello", "client.welcome", "stream.pause", "stream.resume", "state.clear", "state.snapshot", "state.ack", "error"];
|
|
288
301
|
/** True when `name` is a known protocol event. */
|
|
289
302
|
declare function isDevToolsEventName(name: string): name is DevToolsEventName;
|
|
290
303
|
/** Build a fully-typed wire message. */
|
|
@@ -378,4 +391,4 @@ declare function cursorUrl(file: {
|
|
|
378
391
|
column?: number;
|
|
379
392
|
}): string | null;
|
|
380
393
|
|
|
381
|
-
export { type AppMemberNode, type AppModuleNode, type AppSnapshot, type ClientHello, DEFAULT_REDACT_KEYS, DEVTOOLS_EVENTS, type DevToolsEvent, type DevToolsEventMap, type DevToolsEventName, type DevToolsMessage, type EditorKind, type ErrorPayload, type GatewayConnectionPayload, type GatewayMessagePayload, type LogLevel, type LogPayload, MAX_CAPTURE_BYTES, MAX_DEPTH, MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION, type PerformanceSnapshot, type PluginEventPayload, type ProfileCompletedPayload, type ProfileStartedPayload, type ProjectInfo, type QueryPayload, type RedactOptions, Redactor, type RequestCompletedPayload, type RequestStartedPayload, type SourceLocation, type StateSnapshot, type TimelineSpan, buildEditorUrl, createMessage, cursorUrl, defaultRedactor, detectPlatform, isDevToolsEventName, isSupportedProtocolVersion, parseMessage, randomId, redactText, redactValue, vscodeUrl };
|
|
394
|
+
export { type AppMemberNode, type AppModuleNode, type AppSnapshot, type ClientHello, type CompatibilityWarningPayload, DEFAULT_REDACT_KEYS, DEVTOOLS_EVENTS, type DevToolsEvent, type DevToolsEventMap, type DevToolsEventName, type DevToolsMessage, type EditorKind, type ErrorPayload, type GatewayConnectionPayload, type GatewayMessagePayload, type LogLevel, type LogPayload, MAX_CAPTURE_BYTES, MAX_DEPTH, MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION, type PerformanceSnapshot, type PluginEventPayload, type ProfileCompletedPayload, type ProfileStartedPayload, type ProjectInfo, type QueryPayload, type RedactOptions, Redactor, type RequestCompletedPayload, type RequestStartedPayload, type SourceLocation, type StateSnapshot, type TimelineSpan, buildEditorUrl, createMessage, cursorUrl, defaultRedactor, detectPlatform, isDevToolsEventName, isSupportedProtocolVersion, parseMessage, randomId, redactText, redactValue, vscodeUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ interface DevToolsMessage<T = unknown> {
|
|
|
17
17
|
payload: T;
|
|
18
18
|
}
|
|
19
19
|
/** All event names supported by protocol v1. */
|
|
20
|
-
type DevToolsEventName = 'project.connected' | 'project.disconnected' | 'request.started' | 'request.completed' | 'log.created' | 'error.created' | 'query.executed' | 'websocket.connected' | 'websocket.message' | 'performance.updated' | 'profile.started' | 'profile.completed' | 'plugin.event' | 'app.snapshot' | 'client.hello' | 'client.welcome' | 'stream.pause' | 'stream.resume' | 'state.clear' | 'state.snapshot' | 'state.ack' | 'error';
|
|
20
|
+
type DevToolsEventName = 'project.connected' | 'project.disconnected' | 'request.started' | 'request.completed' | 'log.created' | 'error.created' | 'query.executed' | 'websocket.connected' | 'websocket.message' | 'performance.updated' | 'profile.started' | 'profile.completed' | 'plugin.event' | 'compatibility.warning' | 'app.snapshot' | 'client.hello' | 'client.welcome' | 'stream.pause' | 'stream.resume' | 'state.clear' | 'state.snapshot' | 'state.ack' | 'error';
|
|
21
21
|
/** Union of every typed payload keyed by its event name. */
|
|
22
22
|
interface DevToolsEventMap {
|
|
23
23
|
'project.connected': ProjectInfo;
|
|
@@ -36,6 +36,7 @@ interface DevToolsEventMap {
|
|
|
36
36
|
'profile.started': ProfileStartedPayload;
|
|
37
37
|
'profile.completed': ProfileCompletedPayload;
|
|
38
38
|
'plugin.event': PluginEventPayload;
|
|
39
|
+
'compatibility.warning': CompatibilityWarningPayload;
|
|
39
40
|
'app.snapshot': AppSnapshot;
|
|
40
41
|
'client.hello': ClientHello;
|
|
41
42
|
'client.welcome': {
|
|
@@ -114,6 +115,7 @@ interface RequestCompletedPayload {
|
|
|
114
115
|
duration: number;
|
|
115
116
|
startedAt: number;
|
|
116
117
|
timeline: TimelineSpan[];
|
|
118
|
+
requestHeaders?: Record<string, string>;
|
|
117
119
|
query?: Record<string, unknown>;
|
|
118
120
|
headers?: Record<string, string>;
|
|
119
121
|
responsePreview?: string;
|
|
@@ -193,6 +195,7 @@ interface GatewayMessagePayload {
|
|
|
193
195
|
event: string;
|
|
194
196
|
direction: 'received' | 'sent';
|
|
195
197
|
payloadSize: number;
|
|
198
|
+
payloadPreview?: unknown;
|
|
196
199
|
error?: string;
|
|
197
200
|
duration?: number;
|
|
198
201
|
requestId?: string;
|
|
@@ -242,6 +245,16 @@ interface PluginEventPayload {
|
|
|
242
245
|
data?: unknown;
|
|
243
246
|
timestamp: number;
|
|
244
247
|
}
|
|
248
|
+
/** Non-blocking version mismatch reported by the local server. */
|
|
249
|
+
interface CompatibilityWarningPayload {
|
|
250
|
+
projectId: string;
|
|
251
|
+
component: 'sdk' | 'cli' | 'protocol';
|
|
252
|
+
currentVersion: string;
|
|
253
|
+
requiredVersion: string;
|
|
254
|
+
message: string;
|
|
255
|
+
updateCommand: string;
|
|
256
|
+
timestamp: number;
|
|
257
|
+
}
|
|
245
258
|
/** Static description of the NestJS application graph. */
|
|
246
259
|
interface AppSnapshot {
|
|
247
260
|
projectId: string;
|
|
@@ -284,7 +297,7 @@ interface StateSnapshot {
|
|
|
284
297
|
}
|
|
285
298
|
|
|
286
299
|
/** Const array of every event name (runtime mirror of the type union). */
|
|
287
|
-
declare const DEVTOOLS_EVENTS: readonly ["project.connected", "project.disconnected", "request.started", "request.completed", "log.created", "error.created", "query.executed", "websocket.connected", "websocket.message", "performance.updated", "profile.started", "profile.completed", "plugin.event", "app.snapshot", "client.hello", "client.welcome", "stream.pause", "stream.resume", "state.clear", "state.snapshot", "state.ack", "error"];
|
|
300
|
+
declare const DEVTOOLS_EVENTS: readonly ["project.connected", "project.disconnected", "request.started", "request.completed", "log.created", "error.created", "query.executed", "websocket.connected", "websocket.message", "performance.updated", "profile.started", "profile.completed", "plugin.event", "compatibility.warning", "app.snapshot", "client.hello", "client.welcome", "stream.pause", "stream.resume", "state.clear", "state.snapshot", "state.ack", "error"];
|
|
288
301
|
/** True when `name` is a known protocol event. */
|
|
289
302
|
declare function isDevToolsEventName(name: string): name is DevToolsEventName;
|
|
290
303
|
/** Build a fully-typed wire message. */
|
|
@@ -378,4 +391,4 @@ declare function cursorUrl(file: {
|
|
|
378
391
|
column?: number;
|
|
379
392
|
}): string | null;
|
|
380
393
|
|
|
381
|
-
export { type AppMemberNode, type AppModuleNode, type AppSnapshot, type ClientHello, DEFAULT_REDACT_KEYS, DEVTOOLS_EVENTS, type DevToolsEvent, type DevToolsEventMap, type DevToolsEventName, type DevToolsMessage, type EditorKind, type ErrorPayload, type GatewayConnectionPayload, type GatewayMessagePayload, type LogLevel, type LogPayload, MAX_CAPTURE_BYTES, MAX_DEPTH, MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION, type PerformanceSnapshot, type PluginEventPayload, type ProfileCompletedPayload, type ProfileStartedPayload, type ProjectInfo, type QueryPayload, type RedactOptions, Redactor, type RequestCompletedPayload, type RequestStartedPayload, type SourceLocation, type StateSnapshot, type TimelineSpan, buildEditorUrl, createMessage, cursorUrl, defaultRedactor, detectPlatform, isDevToolsEventName, isSupportedProtocolVersion, parseMessage, randomId, redactText, redactValue, vscodeUrl };
|
|
394
|
+
export { type AppMemberNode, type AppModuleNode, type AppSnapshot, type ClientHello, type CompatibilityWarningPayload, DEFAULT_REDACT_KEYS, DEVTOOLS_EVENTS, type DevToolsEvent, type DevToolsEventMap, type DevToolsEventName, type DevToolsMessage, type EditorKind, type ErrorPayload, type GatewayConnectionPayload, type GatewayMessagePayload, type LogLevel, type LogPayload, MAX_CAPTURE_BYTES, MAX_DEPTH, MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION, type PerformanceSnapshot, type PluginEventPayload, type ProfileCompletedPayload, type ProfileStartedPayload, type ProjectInfo, type QueryPayload, type RedactOptions, Redactor, type RequestCompletedPayload, type RequestStartedPayload, type SourceLocation, type StateSnapshot, type TimelineSpan, buildEditorUrl, createMessage, cursorUrl, defaultRedactor, detectPlatform, isDevToolsEventName, isSupportedProtocolVersion, parseMessage, randomId, redactText, redactValue, vscodeUrl };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/events.ts","../src/redact.ts","../src/editor-urls.ts"],"sourcesContent":["/** Wire protocol version. Bump on breaking payload changes. */\nexport const PROTOCOL_VERSION = 1 as const;\nexport const MIN_SUPPORTED_PROTOCOL_VERSION = 1 as const;\n\n/** JSON-RPC-ish envelope shared by every message on the wire. */\nexport interface DevToolsMessage<T = unknown> {\n /** Envelope format version. */\n v: typeof PROTOCOL_VERSION;\n /** Unique message id. */\n id: string;\n /** Originating project (absent for client->server handshake-less control traffic). */\n projectId?: string;\n /** Milliseconds since epoch. */\n ts: number;\n /** Event discriminator. */\n event: DevToolsEventName;\n /** Event payload. */\n payload: T;\n}\n\n/** All event names supported by protocol v1. */\nexport type DevToolsEventName =\n // lifecycle\n | 'project.connected'\n | 'project.disconnected'\n // http\n | 'request.started'\n | 'request.completed'\n // observability\n | 'log.created'\n | 'error.created'\n | 'query.executed'\n // realtime\n | 'websocket.connected'\n | 'websocket.message'\n // perf\n | 'performance.updated'\n | 'profile.started'\n | 'profile.completed'\n | 'plugin.event'\n // application graph\n | 'app.snapshot'\n // control plane\n | 'client.hello'\n | 'client.welcome'\n | 'stream.pause'\n | 'stream.resume'\n | 'state.clear'\n | 'state.snapshot'\n | 'state.ack'\n | 'error';\n\n/** Union of every typed payload keyed by its event name. */\nexport interface DevToolsEventMap {\n 'project.connected': ProjectInfo;\n 'project.disconnected': { projectId: string; reason?: string };\n 'request.started': RequestStartedPayload;\n 'request.completed': RequestCompletedPayload;\n 'log.created': LogPayload;\n 'error.created': ErrorPayload;\n 'query.executed': QueryPayload;\n 'websocket.connected': GatewayConnectionPayload;\n 'websocket.message': GatewayMessagePayload;\n 'performance.updated': PerformanceSnapshot;\n 'profile.started': ProfileStartedPayload;\n 'profile.completed': ProfileCompletedPayload;\n 'plugin.event': PluginEventPayload;\n 'app.snapshot': AppSnapshot;\n 'client.hello': ClientHello;\n 'client.welcome': { serverVersion: string; protocol: typeof PROTOCOL_VERSION };\n 'stream.pause': Record<string, never>;\n 'stream.resume': Record<string, never>;\n 'state.clear': { scope: 'logs' | 'requests' | 'errors' | 'queries' | 'all' };\n 'state.snapshot': StateSnapshot;\n 'state.ack': { ok: true };\n error: { code: string; message: string };\n}\n\n/** Discriminated union of all wire messages. */\nexport type DevToolsEvent = {\n [K in DevToolsEventName]: DevToolsMessage<DevToolsEventMap[K]>;\n}[DevToolsEventName];\n\n/** Metadata every NestJS application reports when it connects. */\nexport interface ProjectInfo {\n projectId: string;\n projectName: string;\n environment: string;\n hostname: string;\n port: number | null;\n pid: number;\n runtime: string;\n runtimeVersion: string;\n nodeVersion: string;\n nestjsVersion: string | null;\n sdkVersion: string;\n}\n\n/** One timeline entry of a request (guard, interceptor, service call...). */\nexport interface TimelineSpan {\n /** Logical layer, e.g. 'middleware' | 'guard' | 'interceptor' | 'pipe' | 'controller' | 'service' | 'database' | 'response'. */\n layer: string;\n /** Human label, e.g. 'JwtAuthGuard' or 'SELECT users'. */\n label: string;\n /** ms */\n duration: number;\n startedAt: number;\n status?: 'ok' | 'error';\n detail?: string;\n /** Source location when available (file, line, column, function). */\n source?: SourceLocation;\n}\n\n/** Emitted the moment a request enters the SDK. */\nexport interface RequestStartedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n httpVersion?: string;\n headers: Record<string, string>;\n query: Record<string, unknown>;\n params?: Record<string, unknown>;\n ip?: string;\n userAgent?: string;\n startedAt: number;\n}\n\n/** Emitted when the response finishes. */\nexport interface RequestCompletedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n statusCode: number;\n duration: number;\n startedAt: number;\n timeline: TimelineSpan[];\n query?: Record<string, unknown>;\n headers?: Record<string, string>;\n responsePreview?: string;\n responseBody?: unknown;\n requestBody?: unknown;\n errored: boolean;\n}\n\n/** A captured console or logger entry. */\nexport interface LogPayload {\n requestId?: string;\n projectId: string;\n level: LogLevel;\n message: string;\n arguments?: unknown[];\n stack?: string;\n source?: SourceLocation;\n context?: string;\n processId: number;\n timestamp: number;\n}\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'verbose';\n\n/** File/line/column triple resolved from source maps when available. */\nexport interface SourceLocation {\n file: string;\n line: number;\n column: number;\n /** Absolute path when resolvable on the host machine. */\n absolutePath?: string;\n /** function name if the stack exposed one */\n function?: string;\n}\n\n/** A captured exception with source mapping. */\nexport interface ErrorPayload {\n requestId?: string;\n projectId: string;\n name: string;\n message: string;\n stack?: string;\n source?: SourceLocation;\n /** stable hash for grouping identical errors */\n fingerprint: string;\n context?: string;\n request?: { method: string; url: string; statusCode?: number };\n controller?: string;\n service?: string;\n timestamp: number;\n /** server-side occurrence count for grouped errors */\n occurrences?: number;\n}\n\n/** A captured database query. */\nexport interface QueryPayload {\n requestId?: string;\n projectId: string;\n provider: 'prisma' | 'typeorm' | 'sequelize' | 'mikroorm' | 'other';\n sql: string;\n duration: number;\n database?: string;\n parameters?: unknown[];\n timestamp: number;\n}\n\n/** Gateway-level connection snapshot. */\nexport interface GatewayConnectionPayload {\n projectId: string;\n gateway: string;\n namespace: string;\n connections: number;\n timestamp: number;\n}\n\n/** Individual gateway event flow. */\nexport interface GatewayMessagePayload {\n projectId: string;\n gateway: string;\n event: string;\n direction: 'received' | 'sent';\n payloadSize: number;\n error?: string;\n duration?: number;\n requestId?: string;\n timestamp: number;\n}\n\n/** Point-in-time process metrics. */\nexport interface PerformanceSnapshot {\n projectId: string;\n timestamp: number;\n cpuPercent: number;\n memoryUsedBytes: number;\n memoryTotalBytes: number;\n heapUsedBytes: number;\n heapTotalBytes: number;\n eventLoopLagMs: number;\n activeRequests: number;\n requestsPerSecond: number;\n averageLatencyMs: number;\n p95LatencyMs: number;\n p99LatencyMs: number;\n errorsPerSecond: number;\n /** true when the process reports memory pressure */\n memoryPressure?: boolean;\n}\n\n/** A profiling session requested by a developer or extension. */\nexport interface ProfileStartedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n durationMs?: number;\n}\n\n/** Result metadata for a completed profiling session. */\nexport interface ProfileCompletedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n completedAt: number;\n data?: unknown;\n}\n\n/** Namespaced event emitted by a registered plugin. */\nexport interface PluginEventPayload {\n projectId: string;\n plugin: string;\n name: string;\n data?: unknown;\n timestamp: number;\n}\n\n/** Static description of the NestJS application graph. */\nexport interface AppSnapshot {\n projectId: string;\n projectName: string;\n modules: AppModuleNode[];\n nestjsVersion: string | null;\n capturedAt: number;\n}\n\n/** A module and its members in the application graph. */\nexport interface AppModuleNode {\n name: string;\n imports: string[];\n controllers: AppMemberNode[];\n providers: AppMemberNode[];\n exports: string[];\n}\n\n/** A member (controller/provider/guard/pipe/...) inside a module. */\nexport interface AppMemberNode {\n name: string;\n type: 'controller' | 'provider' | 'guard' | 'interceptor' | 'pipe' | 'filter' | 'gateway';\n routes?: string[];\n}\n\n/** What a dashboard/control client announces when it connects. */\nexport interface ClientHello {\n kind: 'dashboard' | 'cli' | 'other';\n name?: string;\n version?: string;\n}\n\n/** Full server state handed to newly connected dashboards. */\nexport interface StateSnapshot {\n projects: ProjectInfo[];\n requests: RequestCompletedPayload[];\n logs: LogPayload[];\n errors: ErrorPayload[];\n queries: QueryPayload[];\n websocketConnections: GatewayConnectionPayload[];\n websocketMessages: GatewayMessagePayload[];\n performance: Record<string, PerformanceSnapshot>;\n apps: Record<string, AppSnapshot>;\n}\n","import type { DevToolsEventMap, DevToolsEventName, DevToolsMessage } from './types';\nimport { MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION } from './types';\n\n/** Const array of every event name (runtime mirror of the type union). */\nexport const DEVTOOLS_EVENTS = [\n 'project.connected',\n 'project.disconnected',\n 'request.started',\n 'request.completed',\n 'log.created',\n 'error.created',\n 'query.executed',\n 'websocket.connected',\n 'websocket.message',\n 'performance.updated',\n 'profile.started',\n 'profile.completed',\n 'plugin.event',\n 'app.snapshot',\n 'client.hello',\n 'client.welcome',\n 'stream.pause',\n 'stream.resume',\n 'state.clear',\n 'state.snapshot',\n 'state.ack',\n 'error',\n] as const satisfies readonly DevToolsEventName[];\n\n/** True when `name` is a known protocol event. */\nexport function isDevToolsEventName(name: string): name is DevToolsEventName {\n return (DEVTOOLS_EVENTS as readonly string[]).includes(name);\n}\n\n/** Build a fully-typed wire message. */\nexport function createMessage<K extends DevToolsEventName>(\n event: K,\n payload: DevToolsEventMap[K],\n options?: { projectId?: string; id?: string; ts?: number },\n): DevToolsMessage<DevToolsEventMap[K]> {\n return {\n v: PROTOCOL_VERSION,\n id: options?.id ?? randomId(),\n projectId: options?.projectId,\n ts: options?.ts ?? Date.now(),\n event,\n payload,\n };\n}\n\n/** Cheap collision-resistant id (no crypto dependency needed for wire correlation). */\nexport function randomId(prefix?: string): string {\n const core =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID().replace(/-/g, '').slice(0, 16)\n : Math.random().toString(36).slice(2, 10) + Date.now().toString(36);\n return prefix ? `${prefix}_${core}` : core;\n}\n\n/** Type guard narrowing an unknown parsed frame into a DevToolsMessage. */\nexport function parseMessage(raw: string): DevToolsMessage | null {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'event' in parsed &&\n 'payload' in parsed &&\n 'v' in parsed &&\n Number((parsed as { v: unknown }).v) === PROTOCOL_VERSION &&\n typeof (parsed as { id?: unknown }).id === 'string' &&\n typeof (parsed as { ts?: unknown }).ts === 'number' &&\n isDevToolsEventName(String((parsed as { event: unknown }).event))\n ) {\n return parsed as DevToolsMessage;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** True when a peer can safely communicate with this package. */\nexport function isSupportedProtocolVersion(version: number): boolean {\n return Number.isInteger(version) && version >= MIN_SUPPORTED_PROTOCOL_VERSION && version <= PROTOCOL_VERSION;\n}\n","/** Default keys that are always redacted unless explicitly allowed. */\nexport const DEFAULT_REDACT_KEYS = [\n 'password',\n 'token',\n 'access_token',\n 'accessToken',\n 'refresh_token',\n 'refreshToken',\n 'authorization',\n 'cookie',\n 'cookies',\n 'secret',\n 'apiKey',\n 'api_key',\n 'client_secret',\n 'clientSecret',\n 'private_key',\n 'privateKey',\n 'session',\n 'set-cookie',\n] as const;\n\n/** Default capture ceilings shared by SDK and server. */\nexport const MAX_CAPTURE_BYTES = 4 * 1024;\nexport const MAX_DEPTH = 4;\n\n/** Options for deep redaction of arbitrary values. */\nexport interface RedactOptions {\n /** Extra key names to redact (case-insensitive, partial match allowed). */\n redact?: string[];\n /** Keys that should never be redacted even if they look sensitive. */\n allow?: string[];\n /** Placeholder string used for redacted values. */\n placeholder?: string;\n /** Max serialized size before truncation. */\n maxBytes?: number;\n /** Max object depth. */\n maxDepth?: number;\n}\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]';\nconst TRUNCATION_SUFFIX = '…[truncated]';\n\n/** Normalized set of denylist and allowlist key matchers. */\nexport class Redactor {\n private readonly deny: Set<string>;\n private readonly allow: Set<string>;\n private readonly placeholder: string;\n private readonly maxBytes: number;\n private readonly maxDepth: number;\n\n constructor(options?: RedactOptions) {\n this.deny = new Set([...DEFAULT_REDACT_KEYS, ...(options?.redact ?? [])].map(normalizeKey));\n this.allow = new Set((options?.allow ?? []).map(normalizeKey));\n this.placeholder = options?.placeholder ?? DEFAULT_PLACEHOLDER;\n this.maxBytes = options?.maxBytes ?? MAX_CAPTURE_BYTES;\n this.maxDepth = options?.maxDepth ?? MAX_DEPTH;\n }\n\n /** True when the given key is sensitive and not allowed. */\n isSensitive(key: string): boolean {\n const normalized = normalizeKey(key);\n if (this.allow.has(normalized)) return false;\n if (this.deny.has(normalized)) return true;\n // partial match: `userPassword`, `authTokenValue`, `x-api-key`...\n for (const denied of this.deny) {\n if (normalized.includes(denied)) return true;\n }\n return false;\n }\n\n /**\n * Deep-copy a value while redacting sensitive keys, truncating oversized\n * strings and enforcing depth limits. Circular references become '[Circular]'.\n */\n redact(value: unknown, depth = 0, seen = new Set<unknown>()): unknown {\n if (value === null || typeof value !== 'object') {\n return redactPrimitive(value);\n }\n if (seen.has(value)) return '[Circular]';\n if (depth >= this.maxDepth) return '[MaxDepth]';\n\n seen.add(value);\n try {\n if (Array.isArray(value)) {\n return value.slice(0, 50).map((item) => this.redact(item, depth + 1, seen));\n }\n if (value instanceof Error) {\n return { name: value.name, message: value.message };\n }\n if (value instanceof Date) return value.toISOString();\n\n const out: Record<string, unknown> = {};\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n out[key] = this.isSensitive(key) ? this.placeholder : this.redact(raw, depth + 1, seen);\n }\n return out;\n } finally {\n seen.delete(value);\n }\n }\n\n /** Redact sensitive substrings (key=value / key: value / bearer tokens) from free-form text. */\n redactString(text: string): string {\n let out = text;\n for (const denied of this.deny) {\n const camel = denied;\n const pattern = new RegExp(`(${escapeRegExp(camel)}|${escapeRegExp(denied.replace(/[-_]/g, ''))})\\\\s*[=:]\\\\s*([^\\\\s,&;]+)`, 'gi');\n out = out.replace(pattern, `$1=${this.placeholder}`);\n }\n out = out.replace(/bearer\\s+[a-z0-9._-]+/gi, `Bearer ${this.placeholder}`);\n return out;\n }\n\n /** Serialize a value safely: redacted, size-capped, never throws. */\n serialize(value: unknown): string {\n try {\n const safe = this.redact(value);\n const json = JSON.stringify(safe) ?? String(safe);\n return truncateUtf8(json, this.maxBytes, TRUNCATION_SUFFIX);\n } catch {\n return '[Unserializable]';\n }\n }\n}\n\n/** Shared default redactor instance for quick helpers. */\nexport const defaultRedactor = new Redactor();\n\n/** Convenience: deep-redact with the default policy. */\nexport function redactValue(value: unknown): unknown {\n return defaultRedactor.redact(value);\n}\n\n/** Convenience: scrub a free-form string with the default policy. */\nexport function redactText(text: string): string {\n return defaultRedactor.redactString(text);\n}\n\nfunction redactPrimitive(value: unknown): unknown {\n if (typeof value === 'string') return defaultRedactor.redactString(value);\n if (typeof value === 'number' || typeof value === 'boolean' || value === null) return value;\n if (typeof value === 'function') return '[Function]';\n if (typeof value === 'bigint') return value.toString() + 'n';\n return String(value);\n}\n\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[-_\\s]/g, '');\n}\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** UTF-8-safe truncation that never splits a multi-byte character. */\nfunction truncateUtf8(text: string, maxBytes: number, suffix: string): string {\n const buf = Buffer.from(text, 'utf8');\n if (buf.length <= maxBytes) return text;\n const cut = Math.max(0, maxBytes - Buffer.byteLength(suffix, 'utf8'));\n return buf.subarray(0, cut).toString('utf8') + suffix;\n}\n","/**\n * Deep links that open a file at a specific line/column in an editor.\n * Paths are generated for the OS the DevTools *server* runs on — never assume\n * Windows or POSIX layout; detect it at runtime.\n */\n\nexport type EditorKind = 'vscode' | 'cursor' | 'zed' | 'sublime';\n\nconst EDITOR_SCHEMES: Record<EditorKind, string> = {\n vscode: 'vscode',\n cursor: 'cursor',\n zed: 'zed',\n sublime: 'subl',\n};\n\n/** Returns 'win32' | 'darwin' | 'linux' | 'unknown' without touching process when unavailable. */\nexport function detectPlatform(platformGetter?: () => string): 'win32' | 'darwin' | 'linux' | 'unknown' {\n try {\n const platform = platformGetter ? platformGetter() : process.platform;\n if (platform === 'win32' || platform === 'darwin' || platform === 'linux') return platform;\n return 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\nfunction encodeFilePath(absolutePath: string, platform: string): string {\n // VS Code expects forward slashes; keep drive letters like /C:/... on Windows.\n const normalized = platform === 'win32' ? absolutePath.replace(/\\\\/g, '/') : absolutePath;\n const withDrive = platform === 'win32' && /^[A-Za-z]:\\//.test(normalized) ? `/${normalized}` : normalized;\n // the scheme already carries 'file/', so drop any leading slash;\n // preserve the drive-letter colon (C:) which VS Code expects unencoded\n return withDrive\n .replace(/^\\//, '')\n .split('/')\n .map((segment, index) => (index === 0 && /^[A-Za-z]:$/.test(segment) ? segment : encodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * Build an editor deep link such as:\n * vscode://file/Users/me/app/src/users.service.ts:87:21\n */\nexport function buildEditorUrl(\n editor: EditorKind,\n file: { absolutePath?: string; file?: string; line?: number; column?: number },\n platformGetter?: () => string,\n): string | null {\n const absolutePath = file.absolutePath ?? file.file;\n if (!absolutePath) return null;\n\n const platform = detectPlatform(platformGetter);\n const scheme = EDITOR_SCHEMES[editor];\n const path = encodeFilePath(absolutePath, platform);\n const line = file.line ?? 1;\n const column = file.column ?? 1;\n\n if (editor === 'sublime') {\n return `subl://open?url=file://${path}&line=${line}&column=${column}`;\n }\n return `${scheme}://file/${path}:${line}:${column}`;\n}\n\n/** First available vscode:// link for a source location. */\nexport function vscodeUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('vscode', file);\n}\n\n/** First available cursor:// link for a source location. */\nexport function cursorUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('cursor', file);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/events.ts","../src/redact.ts","../src/editor-urls.ts"],"sourcesContent":["/** Wire protocol version. Bump on breaking payload changes. */\nexport const PROTOCOL_VERSION = 1 as const;\nexport const MIN_SUPPORTED_PROTOCOL_VERSION = 1 as const;\n\n/** JSON-RPC-ish envelope shared by every message on the wire. */\nexport interface DevToolsMessage<T = unknown> {\n /** Envelope format version. */\n v: typeof PROTOCOL_VERSION;\n /** Unique message id. */\n id: string;\n /** Originating project (absent for client->server handshake-less control traffic). */\n projectId?: string;\n /** Milliseconds since epoch. */\n ts: number;\n /** Event discriminator. */\n event: DevToolsEventName;\n /** Event payload. */\n payload: T;\n}\n\n/** All event names supported by protocol v1. */\nexport type DevToolsEventName =\n // lifecycle\n | 'project.connected'\n | 'project.disconnected'\n // http\n | 'request.started'\n | 'request.completed'\n // observability\n | 'log.created'\n | 'error.created'\n | 'query.executed'\n // realtime\n | 'websocket.connected'\n | 'websocket.message'\n // perf\n | 'performance.updated'\n | 'profile.started'\n | 'profile.completed'\n | 'plugin.event'\n | 'compatibility.warning'\n // application graph\n | 'app.snapshot'\n // control plane\n | 'client.hello'\n | 'client.welcome'\n | 'stream.pause'\n | 'stream.resume'\n | 'state.clear'\n | 'state.snapshot'\n | 'state.ack'\n | 'error';\n\n/** Union of every typed payload keyed by its event name. */\nexport interface DevToolsEventMap {\n 'project.connected': ProjectInfo;\n 'project.disconnected': { projectId: string; reason?: string };\n 'request.started': RequestStartedPayload;\n 'request.completed': RequestCompletedPayload;\n 'log.created': LogPayload;\n 'error.created': ErrorPayload;\n 'query.executed': QueryPayload;\n 'websocket.connected': GatewayConnectionPayload;\n 'websocket.message': GatewayMessagePayload;\n 'performance.updated': PerformanceSnapshot;\n 'profile.started': ProfileStartedPayload;\n 'profile.completed': ProfileCompletedPayload;\n 'plugin.event': PluginEventPayload;\n 'compatibility.warning': CompatibilityWarningPayload;\n 'app.snapshot': AppSnapshot;\n 'client.hello': ClientHello;\n 'client.welcome': { serverVersion: string; protocol: typeof PROTOCOL_VERSION };\n 'stream.pause': Record<string, never>;\n 'stream.resume': Record<string, never>;\n 'state.clear': { scope: 'logs' | 'requests' | 'errors' | 'queries' | 'all' };\n 'state.snapshot': StateSnapshot;\n 'state.ack': { ok: true };\n error: { code: string; message: string };\n}\n\n/** Discriminated union of all wire messages. */\nexport type DevToolsEvent = {\n [K in DevToolsEventName]: DevToolsMessage<DevToolsEventMap[K]>;\n}[DevToolsEventName];\n\n/** Metadata every NestJS application reports when it connects. */\nexport interface ProjectInfo {\n projectId: string;\n projectName: string;\n environment: string;\n hostname: string;\n port: number | null;\n pid: number;\n runtime: string;\n runtimeVersion: string;\n nodeVersion: string;\n nestjsVersion: string | null;\n sdkVersion: string;\n}\n\n/** One timeline entry of a request (guard, interceptor, service call...). */\nexport interface TimelineSpan {\n /** Logical layer, e.g. 'middleware' | 'guard' | 'interceptor' | 'pipe' | 'controller' | 'service' | 'database' | 'response'. */\n layer: string;\n /** Human label, e.g. 'JwtAuthGuard' or 'SELECT users'. */\n label: string;\n /** ms */\n duration: number;\n startedAt: number;\n status?: 'ok' | 'error';\n detail?: string;\n /** Source location when available (file, line, column, function). */\n source?: SourceLocation;\n}\n\n/** Emitted the moment a request enters the SDK. */\nexport interface RequestStartedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n httpVersion?: string;\n headers: Record<string, string>;\n query: Record<string, unknown>;\n params?: Record<string, unknown>;\n ip?: string;\n userAgent?: string;\n startedAt: number;\n}\n\n/** Emitted when the response finishes. */\nexport interface RequestCompletedPayload {\n requestId: string;\n projectId: string;\n method: string;\n url: string;\n route?: string;\n statusCode: number;\n duration: number;\n startedAt: number;\n timeline: TimelineSpan[];\n requestHeaders?: Record<string, string>;\n query?: Record<string, unknown>;\n headers?: Record<string, string>;\n responsePreview?: string;\n responseBody?: unknown;\n requestBody?: unknown;\n errored: boolean;\n}\n\n/** A captured console or logger entry. */\nexport interface LogPayload {\n requestId?: string;\n projectId: string;\n level: LogLevel;\n message: string;\n arguments?: unknown[];\n stack?: string;\n source?: SourceLocation;\n context?: string;\n processId: number;\n timestamp: number;\n}\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'verbose';\n\n/** File/line/column triple resolved from source maps when available. */\nexport interface SourceLocation {\n file: string;\n line: number;\n column: number;\n /** Absolute path when resolvable on the host machine. */\n absolutePath?: string;\n /** function name if the stack exposed one */\n function?: string;\n}\n\n/** A captured exception with source mapping. */\nexport interface ErrorPayload {\n requestId?: string;\n projectId: string;\n name: string;\n message: string;\n stack?: string;\n source?: SourceLocation;\n /** stable hash for grouping identical errors */\n fingerprint: string;\n context?: string;\n request?: { method: string; url: string; statusCode?: number };\n controller?: string;\n service?: string;\n timestamp: number;\n /** server-side occurrence count for grouped errors */\n occurrences?: number;\n}\n\n/** A captured database query. */\nexport interface QueryPayload {\n requestId?: string;\n projectId: string;\n provider: 'prisma' | 'typeorm' | 'sequelize' | 'mikroorm' | 'other';\n sql: string;\n duration: number;\n database?: string;\n parameters?: unknown[];\n timestamp: number;\n}\n\n/** Gateway-level connection snapshot. */\nexport interface GatewayConnectionPayload {\n projectId: string;\n gateway: string;\n namespace: string;\n connections: number;\n timestamp: number;\n}\n\n/** Individual gateway event flow. */\nexport interface GatewayMessagePayload {\n projectId: string;\n gateway: string;\n event: string;\n direction: 'received' | 'sent';\n payloadSize: number;\n payloadPreview?: unknown;\n error?: string;\n duration?: number;\n requestId?: string;\n timestamp: number;\n}\n\n/** Point-in-time process metrics. */\nexport interface PerformanceSnapshot {\n projectId: string;\n timestamp: number;\n cpuPercent: number;\n memoryUsedBytes: number;\n memoryTotalBytes: number;\n heapUsedBytes: number;\n heapTotalBytes: number;\n eventLoopLagMs: number;\n activeRequests: number;\n requestsPerSecond: number;\n averageLatencyMs: number;\n p95LatencyMs: number;\n p99LatencyMs: number;\n errorsPerSecond: number;\n /** true when the process reports memory pressure */\n memoryPressure?: boolean;\n}\n\n/** A profiling session requested by a developer or extension. */\nexport interface ProfileStartedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n durationMs?: number;\n}\n\n/** Result metadata for a completed profiling session. */\nexport interface ProfileCompletedPayload {\n projectId: string;\n profileId: string;\n kind: 'cpu' | 'heap';\n startedAt: number;\n completedAt: number;\n data?: unknown;\n}\n\n/** Namespaced event emitted by a registered plugin. */\nexport interface PluginEventPayload {\n projectId: string;\n plugin: string;\n name: string;\n data?: unknown;\n timestamp: number;\n}\n\n/** Non-blocking version mismatch reported by the local server. */\nexport interface CompatibilityWarningPayload {\n projectId: string;\n component: 'sdk' | 'cli' | 'protocol';\n currentVersion: string;\n requiredVersion: string;\n message: string;\n updateCommand: string;\n timestamp: number;\n}\n\n/** Static description of the NestJS application graph. */\nexport interface AppSnapshot {\n projectId: string;\n projectName: string;\n modules: AppModuleNode[];\n nestjsVersion: string | null;\n capturedAt: number;\n}\n\n/** A module and its members in the application graph. */\nexport interface AppModuleNode {\n name: string;\n imports: string[];\n controllers: AppMemberNode[];\n providers: AppMemberNode[];\n exports: string[];\n}\n\n/** A member (controller/provider/guard/pipe/...) inside a module. */\nexport interface AppMemberNode {\n name: string;\n type: 'controller' | 'provider' | 'guard' | 'interceptor' | 'pipe' | 'filter' | 'gateway';\n routes?: string[];\n}\n\n/** What a dashboard/control client announces when it connects. */\nexport interface ClientHello {\n kind: 'dashboard' | 'cli' | 'other';\n name?: string;\n version?: string;\n}\n\n/** Full server state handed to newly connected dashboards. */\nexport interface StateSnapshot {\n projects: ProjectInfo[];\n requests: RequestCompletedPayload[];\n logs: LogPayload[];\n errors: ErrorPayload[];\n queries: QueryPayload[];\n websocketConnections: GatewayConnectionPayload[];\n websocketMessages: GatewayMessagePayload[];\n performance: Record<string, PerformanceSnapshot>;\n apps: Record<string, AppSnapshot>;\n}\n","import type { DevToolsEventMap, DevToolsEventName, DevToolsMessage } from './types';\nimport { MIN_SUPPORTED_PROTOCOL_VERSION, PROTOCOL_VERSION } from './types';\n\n/** Const array of every event name (runtime mirror of the type union). */\nexport const DEVTOOLS_EVENTS = [\n 'project.connected',\n 'project.disconnected',\n 'request.started',\n 'request.completed',\n 'log.created',\n 'error.created',\n 'query.executed',\n 'websocket.connected',\n 'websocket.message',\n 'performance.updated',\n 'profile.started',\n 'profile.completed',\n 'plugin.event',\n 'compatibility.warning',\n 'app.snapshot',\n 'client.hello',\n 'client.welcome',\n 'stream.pause',\n 'stream.resume',\n 'state.clear',\n 'state.snapshot',\n 'state.ack',\n 'error',\n] as const satisfies readonly DevToolsEventName[];\n\n/** True when `name` is a known protocol event. */\nexport function isDevToolsEventName(name: string): name is DevToolsEventName {\n return (DEVTOOLS_EVENTS as readonly string[]).includes(name);\n}\n\n/** Build a fully-typed wire message. */\nexport function createMessage<K extends DevToolsEventName>(\n event: K,\n payload: DevToolsEventMap[K],\n options?: { projectId?: string; id?: string; ts?: number },\n): DevToolsMessage<DevToolsEventMap[K]> {\n return {\n v: PROTOCOL_VERSION,\n id: options?.id ?? randomId(),\n projectId: options?.projectId,\n ts: options?.ts ?? Date.now(),\n event,\n payload,\n };\n}\n\n/** Cheap collision-resistant id (no crypto dependency needed for wire correlation). */\nexport function randomId(prefix?: string): string {\n const core =\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID().replace(/-/g, '').slice(0, 16)\n : Math.random().toString(36).slice(2, 10) + Date.now().toString(36);\n return prefix ? `${prefix}_${core}` : core;\n}\n\n/** Type guard narrowing an unknown parsed frame into a DevToolsMessage. */\nexport function parseMessage(raw: string): DevToolsMessage | null {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'event' in parsed &&\n 'payload' in parsed &&\n 'v' in parsed &&\n Number((parsed as { v: unknown }).v) === PROTOCOL_VERSION &&\n typeof (parsed as { id?: unknown }).id === 'string' &&\n typeof (parsed as { ts?: unknown }).ts === 'number' &&\n isDevToolsEventName(String((parsed as { event: unknown }).event))\n ) {\n return parsed as DevToolsMessage;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** True when a peer can safely communicate with this package. */\nexport function isSupportedProtocolVersion(version: number): boolean {\n return Number.isInteger(version) && version >= MIN_SUPPORTED_PROTOCOL_VERSION && version <= PROTOCOL_VERSION;\n}\n","/** Default keys that are always redacted unless explicitly allowed. */\nexport const DEFAULT_REDACT_KEYS = [\n 'password',\n 'token',\n 'access_token',\n 'accessToken',\n 'refresh_token',\n 'refreshToken',\n 'authorization',\n 'cookie',\n 'cookies',\n 'secret',\n 'apiKey',\n 'api_key',\n 'client_secret',\n 'clientSecret',\n 'private_key',\n 'privateKey',\n 'session',\n 'set-cookie',\n] as const;\n\n/** Default capture ceilings shared by SDK and server. */\nexport const MAX_CAPTURE_BYTES = 4 * 1024;\nexport const MAX_DEPTH = 4;\n\n/** Options for deep redaction of arbitrary values. */\nexport interface RedactOptions {\n /** Extra key names to redact (case-insensitive, partial match allowed). */\n redact?: string[];\n /** Keys that should never be redacted even if they look sensitive. */\n allow?: string[];\n /** Placeholder string used for redacted values. */\n placeholder?: string;\n /** Max serialized size before truncation. */\n maxBytes?: number;\n /** Max object depth. */\n maxDepth?: number;\n}\n\nconst DEFAULT_PLACEHOLDER = '[REDACTED]';\nconst TRUNCATION_SUFFIX = '…[truncated]';\n\n/** Normalized set of denylist and allowlist key matchers. */\nexport class Redactor {\n private readonly deny: Set<string>;\n private readonly allow: Set<string>;\n private readonly placeholder: string;\n private readonly maxBytes: number;\n private readonly maxDepth: number;\n\n constructor(options?: RedactOptions) {\n this.deny = new Set([...DEFAULT_REDACT_KEYS, ...(options?.redact ?? [])].map(normalizeKey));\n this.allow = new Set((options?.allow ?? []).map(normalizeKey));\n this.placeholder = options?.placeholder ?? DEFAULT_PLACEHOLDER;\n this.maxBytes = options?.maxBytes ?? MAX_CAPTURE_BYTES;\n this.maxDepth = options?.maxDepth ?? MAX_DEPTH;\n }\n\n /** True when the given key is sensitive and not allowed. */\n isSensitive(key: string): boolean {\n const normalized = normalizeKey(key);\n if (this.allow.has(normalized)) return false;\n if (this.deny.has(normalized)) return true;\n // partial match: `userPassword`, `authTokenValue`, `x-api-key`...\n for (const denied of this.deny) {\n if (normalized.includes(denied)) return true;\n }\n return false;\n }\n\n /**\n * Deep-copy a value while redacting sensitive keys, truncating oversized\n * strings and enforcing depth limits. Circular references become '[Circular]'.\n */\n redact(value: unknown, depth = 0, seen = new Set<unknown>()): unknown {\n if (value === null || typeof value !== 'object') {\n return redactPrimitive(value);\n }\n if (seen.has(value)) return '[Circular]';\n if (depth >= this.maxDepth) return '[MaxDepth]';\n\n seen.add(value);\n try {\n if (Array.isArray(value)) {\n return value.slice(0, 50).map((item) => this.redact(item, depth + 1, seen));\n }\n if (value instanceof Error) {\n return { name: value.name, message: value.message };\n }\n if (value instanceof Date) return value.toISOString();\n\n const out: Record<string, unknown> = {};\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n out[key] = this.isSensitive(key) ? this.placeholder : this.redact(raw, depth + 1, seen);\n }\n return out;\n } finally {\n seen.delete(value);\n }\n }\n\n /** Redact sensitive substrings (key=value / key: value / bearer tokens) from free-form text. */\n redactString(text: string): string {\n let out = text;\n for (const denied of this.deny) {\n const camel = denied;\n const pattern = new RegExp(`(${escapeRegExp(camel)}|${escapeRegExp(denied.replace(/[-_]/g, ''))})\\\\s*[=:]\\\\s*([^\\\\s,&;]+)`, 'gi');\n out = out.replace(pattern, `$1=${this.placeholder}`);\n }\n out = out.replace(/bearer\\s+[a-z0-9._-]+/gi, `Bearer ${this.placeholder}`);\n return out;\n }\n\n /** Serialize a value safely: redacted, size-capped, never throws. */\n serialize(value: unknown): string {\n try {\n const safe = this.redact(value);\n const json = JSON.stringify(safe) ?? String(safe);\n return truncateUtf8(json, this.maxBytes, TRUNCATION_SUFFIX);\n } catch {\n return '[Unserializable]';\n }\n }\n}\n\n/** Shared default redactor instance for quick helpers. */\nexport const defaultRedactor = new Redactor();\n\n/** Convenience: deep-redact with the default policy. */\nexport function redactValue(value: unknown): unknown {\n return defaultRedactor.redact(value);\n}\n\n/** Convenience: scrub a free-form string with the default policy. */\nexport function redactText(text: string): string {\n return defaultRedactor.redactString(text);\n}\n\nfunction redactPrimitive(value: unknown): unknown {\n if (typeof value === 'string') return defaultRedactor.redactString(value);\n if (typeof value === 'number' || typeof value === 'boolean' || value === null) return value;\n if (typeof value === 'function') return '[Function]';\n if (typeof value === 'bigint') return value.toString() + 'n';\n return String(value);\n}\n\nfunction normalizeKey(key: string): string {\n return key.toLowerCase().replace(/[-_\\s]/g, '');\n}\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** UTF-8-safe truncation that never splits a multi-byte character. */\nfunction truncateUtf8(text: string, maxBytes: number, suffix: string): string {\n const buf = Buffer.from(text, 'utf8');\n if (buf.length <= maxBytes) return text;\n const cut = Math.max(0, maxBytes - Buffer.byteLength(suffix, 'utf8'));\n return buf.subarray(0, cut).toString('utf8') + suffix;\n}\n","/**\n * Deep links that open a file at a specific line/column in an editor.\n * Paths are generated for the OS the DevTools *server* runs on — never assume\n * Windows or POSIX layout; detect it at runtime.\n */\n\nexport type EditorKind = 'vscode' | 'cursor' | 'zed' | 'sublime';\n\nconst EDITOR_SCHEMES: Record<EditorKind, string> = {\n vscode: 'vscode',\n cursor: 'cursor',\n zed: 'zed',\n sublime: 'subl',\n};\n\n/** Returns 'win32' | 'darwin' | 'linux' | 'unknown' without touching process when unavailable. */\nexport function detectPlatform(platformGetter?: () => string): 'win32' | 'darwin' | 'linux' | 'unknown' {\n try {\n const platform = platformGetter ? platformGetter() : process.platform;\n if (platform === 'win32' || platform === 'darwin' || platform === 'linux') return platform;\n return 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\nfunction encodeFilePath(absolutePath: string, platform: string): string {\n // VS Code expects forward slashes; keep drive letters like /C:/... on Windows.\n const normalized = platform === 'win32' ? absolutePath.replace(/\\\\/g, '/') : absolutePath;\n const withDrive = platform === 'win32' && /^[A-Za-z]:\\//.test(normalized) ? `/${normalized}` : normalized;\n // the scheme already carries 'file/', so drop any leading slash;\n // preserve the drive-letter colon (C:) which VS Code expects unencoded\n return withDrive\n .replace(/^\\//, '')\n .split('/')\n .map((segment, index) => (index === 0 && /^[A-Za-z]:$/.test(segment) ? segment : encodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * Build an editor deep link such as:\n * vscode://file/Users/me/app/src/users.service.ts:87:21\n */\nexport function buildEditorUrl(\n editor: EditorKind,\n file: { absolutePath?: string; file?: string; line?: number; column?: number },\n platformGetter?: () => string,\n): string | null {\n const absolutePath = file.absolutePath ?? file.file;\n if (!absolutePath) return null;\n\n const platform = detectPlatform(platformGetter);\n const scheme = EDITOR_SCHEMES[editor];\n const path = encodeFilePath(absolutePath, platform);\n const line = file.line ?? 1;\n const column = file.column ?? 1;\n\n if (editor === 'sublime') {\n return `subl://open?url=file://${path}&line=${line}&column=${column}`;\n }\n return `${scheme}://file/${path}:${line}:${column}`;\n}\n\n/** First available vscode:// link for a source location. */\nexport function vscodeUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('vscode', file);\n}\n\n/** First available cursor:// link for a source location. */\nexport function cursorUrl(file: { absolutePath?: string; file?: string; line?: number; column?: number }): string | null {\n return buildEditorUrl('cursor', file);\n}\n"],"mappings":"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","names":[]}
|