@rawnodes/logger 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import { Writable } from 'node:stream';
2
+ import { R as RelayConfig } from '../types-lfJLhC8J.mjs';
3
+
4
+ /**
5
+ * Returns true iff `wsUrl`'s origin matches `apiUrl`'s origin OR its hostname
6
+ * is in the explicit allowlist. Defends against a compromised /api/relay/config
7
+ * endpoint pointing log streams at an attacker-controlled WebSocket.
8
+ *
9
+ * @internal Exported for tests.
10
+ */
11
+ declare function isWsUrlAllowed(wsUrl: string, apiUrl: string, allowedHosts: string[] | undefined): boolean;
12
+ declare function export_default(opts: RelayConfig): Writable;
13
+
14
+ export { export_default as default, isWsUrlAllowed };
@@ -0,0 +1,14 @@
1
+ import { Writable } from 'node:stream';
2
+ import { R as RelayConfig } from '../types-lfJLhC8J.js';
3
+
4
+ /**
5
+ * Returns true iff `wsUrl`'s origin matches `apiUrl`'s origin OR its hostname
6
+ * is in the explicit allowlist. Defends against a compromised /api/relay/config
7
+ * endpoint pointing log streams at an attacker-controlled WebSocket.
8
+ *
9
+ * @internal Exported for tests.
10
+ */
11
+ declare function isWsUrlAllowed(wsUrl: string, apiUrl: string, allowedHosts: string[] | undefined): boolean;
12
+ declare function export_default(opts: RelayConfig): Writable;
13
+
14
+ export { export_default as default, isWsUrlAllowed };
@@ -0,0 +1,368 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var stream = require('stream');
6
+
7
+ // src/transports/relay.ts
8
+
9
+ // src/utils/mask-secrets.ts
10
+ var DEFAULT_SECRET_PATTERNS = [
11
+ "password",
12
+ "secret",
13
+ "token",
14
+ "apikey",
15
+ "api_key",
16
+ "api-key",
17
+ "auth",
18
+ "credential",
19
+ "private"
20
+ ];
21
+ var DEFAULT_MASK = "***";
22
+ var REGEX_ESCAPE_RE = /[.*+?^${}()|[\]\\]/g;
23
+ function buildSecretRe(patterns) {
24
+ const escaped = patterns.map((p) => p.replace(REGEX_ESCAPE_RE, "\\$&"));
25
+ return new RegExp(escaped.join("|"), "i");
26
+ }
27
+ var DEFAULT_SECRET_RE = buildSecretRe(DEFAULT_SECRET_PATTERNS);
28
+ var DEFAULT_RESOLVED = {
29
+ secretRe: DEFAULT_SECRET_RE,
30
+ mask: DEFAULT_MASK,
31
+ deep: true
32
+ };
33
+ function resolveOptions(options) {
34
+ if (options.patterns === void 0 && options.mask === void 0 && options.deep === void 0) {
35
+ return DEFAULT_RESOLVED;
36
+ }
37
+ const { patterns = DEFAULT_SECRET_PATTERNS, mask = DEFAULT_MASK, deep = true } = options;
38
+ return {
39
+ secretRe: patterns === DEFAULT_SECRET_PATTERNS ? DEFAULT_SECRET_RE : buildSecretRe(patterns),
40
+ mask,
41
+ deep
42
+ };
43
+ }
44
+ function maskUrlCredentials(url, mask) {
45
+ try {
46
+ const parsed = new URL(url);
47
+ if (parsed.password) {
48
+ parsed.password = mask;
49
+ }
50
+ if (parsed.username && parsed.password) {
51
+ parsed.username = mask;
52
+ }
53
+ return parsed.toString();
54
+ } catch {
55
+ return url;
56
+ }
57
+ }
58
+ function maskReplacer(options = {}) {
59
+ const { secretRe, mask } = resolveOptions(options);
60
+ return function(key, value) {
61
+ if (key !== "" && secretRe.test(key)) return mask;
62
+ if (typeof value !== "string") return value;
63
+ if (value.length < 10) return value;
64
+ if (value.charCodeAt(0) !== 104) return value;
65
+ if (!value.startsWith("http://") && !value.startsWith("https://")) return value;
66
+ if (value.indexOf("@") === -1) return value;
67
+ return maskUrlCredentials(value, mask);
68
+ };
69
+ }
70
+
71
+ // src/transports/relay.ts
72
+ var LOG_LEVELS = {
73
+ off: -1,
74
+ error: 0,
75
+ warn: 1,
76
+ info: 2,
77
+ http: 3,
78
+ verbose: 4,
79
+ debug: 5,
80
+ silly: 6
81
+ };
82
+ function getLevelName(pinoLevel) {
83
+ if (pinoLevel >= 50) return "error";
84
+ if (pinoLevel >= 40) return "warn";
85
+ if (pinoLevel >= 30) return "info";
86
+ if (pinoLevel >= 25) return "http";
87
+ if (pinoLevel >= 20) return "debug";
88
+ return "silly";
89
+ }
90
+ var DEFAULTS = {
91
+ pollInterval: 3e4,
92
+ bufferSize: 1e3,
93
+ reconnectDelay: 1e3,
94
+ maxReconnectDelay: 3e4
95
+ };
96
+ var RingBuffer = class {
97
+ constructor(capacity) {
98
+ this.capacity = capacity;
99
+ this.items = new Array(capacity);
100
+ }
101
+ items;
102
+ head = 0;
103
+ count = 0;
104
+ push(item) {
105
+ this.items[(this.head + this.count) % this.capacity] = item;
106
+ if (this.count < this.capacity) {
107
+ this.count++;
108
+ } else {
109
+ this.head = (this.head + 1) % this.capacity;
110
+ }
111
+ }
112
+ drain() {
113
+ const result = [];
114
+ for (let i = 0; i < this.count; i++) {
115
+ result.push(this.items[(this.head + i) % this.capacity]);
116
+ }
117
+ this.head = 0;
118
+ this.count = 0;
119
+ return result;
120
+ }
121
+ get size() {
122
+ return this.count;
123
+ }
124
+ };
125
+ function matchesRules(log, rules) {
126
+ if (!rules || rules.length === 0) return true;
127
+ return rules.some((rule) => {
128
+ if (rule.level) {
129
+ const logLevelNum = LOG_LEVELS[getLevelName(log.level)] ?? 6;
130
+ const ruleLevelNum = LOG_LEVELS[rule.level] ?? 6;
131
+ if (logLevelNum > ruleLevelNum) return false;
132
+ }
133
+ if (rule.context && log.context !== rule.context) return false;
134
+ if (rule.match) {
135
+ for (const [key, value] of Object.entries(rule.match)) {
136
+ if (log[key] !== value) return false;
137
+ }
138
+ }
139
+ return true;
140
+ });
141
+ }
142
+ function isWsUrlAllowed(wsUrl, apiUrl, allowedHosts) {
143
+ let parsed;
144
+ let api;
145
+ try {
146
+ parsed = new URL(wsUrl);
147
+ api = new URL(apiUrl);
148
+ } catch {
149
+ return false;
150
+ }
151
+ if (parsed.hostname === api.hostname) return true;
152
+ if (allowedHosts && allowedHosts.includes(parsed.hostname)) return true;
153
+ return false;
154
+ }
155
+ function relay_default(opts) {
156
+ const pollInterval = opts.pollInterval ?? DEFAULTS.pollInterval;
157
+ const bufferSize = opts.bufferSize ?? DEFAULTS.bufferSize;
158
+ const reconnectDelay = opts.reconnectDelay ?? DEFAULTS.reconnectDelay;
159
+ const maxReconnectDelay = opts.maxReconnectDelay ?? DEFAULTS.maxReconnectDelay;
160
+ let replacer;
161
+ if (opts.maskSecrets) {
162
+ const maskOpts = opts.maskSecrets === true ? {} : opts.maskSecrets;
163
+ replacer = maskReplacer(maskOpts);
164
+ }
165
+ let state = "POLLING";
166
+ let serverConfig = null;
167
+ let ws = null;
168
+ let pollTimer = null;
169
+ let reconnectTimer = null;
170
+ let reconnectAttempt = 0;
171
+ let closed = false;
172
+ let announcedStreamingFor = null;
173
+ const buffer = new RingBuffer(bufferSize);
174
+ function maskLine(line) {
175
+ if (!replacer) return line;
176
+ try {
177
+ const parsed = JSON.parse(line);
178
+ return JSON.stringify(parsed, replacer);
179
+ } catch {
180
+ return line;
181
+ }
182
+ }
183
+ let WebSocketClass = null;
184
+ async function loadWebSocket() {
185
+ if (WebSocketClass) return WebSocketClass;
186
+ try {
187
+ const wsModule = await import('ws');
188
+ WebSocketClass = wsModule.default;
189
+ return WebSocketClass;
190
+ } catch {
191
+ throw new Error(
192
+ '@rawnodes/logger relay transport requires the "ws" package. Install it: npm install ws'
193
+ );
194
+ }
195
+ }
196
+ function sendLine(line) {
197
+ if (ws && ws.readyState === 1) {
198
+ if (ws.bufferedAmount > 1024 * 1024) return;
199
+ ws.send(maskLine(line));
200
+ }
201
+ }
202
+ function flushBuffer() {
203
+ const lines = buffer.drain();
204
+ for (const line of lines) {
205
+ sendLine(line);
206
+ }
207
+ }
208
+ async function openWebSocket(url) {
209
+ if (closed) return;
210
+ state = "CONNECTING";
211
+ reconnectAttempt = 0;
212
+ try {
213
+ const WS = await loadWebSocket();
214
+ const socket = new WS(url, {
215
+ headers: { authorization: `Bearer ${opts.token}` }
216
+ });
217
+ ws = socket;
218
+ socket.on("open", () => {
219
+ state = "STREAMING";
220
+ reconnectAttempt = 0;
221
+ if (announcedStreamingFor !== url) {
222
+ announcedStreamingFor = url;
223
+ process.stderr.write(
224
+ `[RelayTransport] streaming logs to ${url}` + (replacer ? " (maskSecrets: on)" : "") + "\n"
225
+ );
226
+ }
227
+ flushBuffer();
228
+ });
229
+ socket.on("close", () => {
230
+ if (!closed) scheduleReconnect(url);
231
+ });
232
+ socket.on("error", (err) => {
233
+ console.error("[RelayTransport] WebSocket error:", err.message);
234
+ socket.close();
235
+ });
236
+ socket.on("message", (data) => {
237
+ try {
238
+ const msg = JSON.parse(data.toString());
239
+ if (msg.type === "config" && msg.rules) {
240
+ if (serverConfig) serverConfig.rules = msg.rules;
241
+ }
242
+ if (msg.type === "config" && msg.enabled === false) {
243
+ closeWebSocket();
244
+ serverConfig = { enabled: false };
245
+ }
246
+ } catch {
247
+ }
248
+ });
249
+ } catch (err) {
250
+ console.error("[RelayTransport]", err.message);
251
+ if (!closed) scheduleReconnect(url);
252
+ }
253
+ }
254
+ function scheduleReconnect(url) {
255
+ if (closed) return;
256
+ state = "RECONNECTING";
257
+ const delay = Math.min(reconnectDelay * Math.pow(2, reconnectAttempt), maxReconnectDelay);
258
+ reconnectAttempt++;
259
+ reconnectTimer = setTimeout(() => {
260
+ reconnectTimer = null;
261
+ void openWebSocket(url);
262
+ }, delay);
263
+ }
264
+ function closeWebSocket() {
265
+ if (ws) {
266
+ ws.removeAllListeners();
267
+ if (ws.readyState === 0 || ws.readyState === 1) {
268
+ ws.close();
269
+ }
270
+ ws = null;
271
+ }
272
+ if (reconnectTimer) {
273
+ clearTimeout(reconnectTimer);
274
+ reconnectTimer = null;
275
+ }
276
+ state = "POLLING";
277
+ }
278
+ async function poll() {
279
+ if (closed) return;
280
+ try {
281
+ const res = await fetch(`${opts.apiUrl}/api/relay/config`, {
282
+ headers: { authorization: `Bearer ${opts.token}` },
283
+ signal: AbortSignal.timeout(1e4)
284
+ });
285
+ if (!res.ok) return;
286
+ const config = await res.json();
287
+ serverConfig = config;
288
+ if (config.enabled && config.wsUrl) {
289
+ if (!isWsUrlAllowed(config.wsUrl, opts.apiUrl, opts.allowedWsHosts)) {
290
+ process.stderr.write(
291
+ `[RelayTransport] refusing wsUrl "${config.wsUrl}" \u2014 origin does not match apiUrl "${opts.apiUrl}" and host is not in allowedWsHosts. Set RelayConfig.allowedWsHosts to authorize alternate hosts.
292
+ `
293
+ );
294
+ return;
295
+ }
296
+ if (state === "POLLING") {
297
+ void openWebSocket(config.wsUrl);
298
+ }
299
+ } else {
300
+ if (state !== "POLLING") {
301
+ closeWebSocket();
302
+ }
303
+ }
304
+ } catch {
305
+ }
306
+ }
307
+ void poll();
308
+ pollTimer = setInterval(() => void poll(), pollInterval);
309
+ const stream$1 = new stream.Writable({
310
+ write(chunk, _encoding, callback) {
311
+ if (closed) {
312
+ callback();
313
+ return;
314
+ }
315
+ const line = chunk.toString().trim();
316
+ if (!line) {
317
+ callback();
318
+ return;
319
+ }
320
+ if (state === "POLLING" || !serverConfig?.enabled) {
321
+ callback();
322
+ return;
323
+ }
324
+ if (serverConfig.rules && serverConfig.rules.length > 0) {
325
+ try {
326
+ const log = JSON.parse(line);
327
+ if (!matchesRules(log, serverConfig.rules)) {
328
+ callback();
329
+ return;
330
+ }
331
+ } catch {
332
+ callback();
333
+ return;
334
+ }
335
+ }
336
+ if (state === "STREAMING") {
337
+ sendLine(line);
338
+ } else if (state === "RECONNECTING" || state === "CONNECTING") {
339
+ buffer.push(line);
340
+ }
341
+ callback();
342
+ },
343
+ final(callback) {
344
+ closed = true;
345
+ if (pollTimer) {
346
+ clearInterval(pollTimer);
347
+ pollTimer = null;
348
+ }
349
+ closeWebSocket();
350
+ callback();
351
+ },
352
+ destroy(error, callback) {
353
+ closed = true;
354
+ if (pollTimer) {
355
+ clearInterval(pollTimer);
356
+ pollTimer = null;
357
+ }
358
+ closeWebSocket();
359
+ callback(error);
360
+ }
361
+ });
362
+ return stream$1;
363
+ }
364
+
365
+ exports.default = relay_default;
366
+ exports.isWsUrlAllowed = isWsUrlAllowed;
367
+ //# sourceMappingURL=relay.js.map
368
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/mask-secrets.ts","../../src/transports/relay.ts"],"names":["stream","Writable"],"mappings":"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","file":"relay.js","sourcesContent":["const DEFAULT_SECRET_PATTERNS = [\n 'password',\n 'secret',\n 'token',\n 'apikey',\n 'api_key',\n 'api-key',\n 'auth',\n 'credential',\n 'private',\n];\n\nconst DEFAULT_MASK = '***';\n\nexport interface MaskSecretsOptions {\n patterns?: string[];\n mask?: string;\n deep?: boolean;\n}\n\ninterface ResolvedOptions {\n /** Single combined case-insensitive regex; one .test() per key replaces N includes() + toLowerCase. */\n secretRe: RegExp;\n mask: string;\n deep: boolean;\n}\n\nconst REGEX_ESCAPE_RE = /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction buildSecretRe(patterns: string[]): RegExp {\n const escaped = patterns.map((p) => p.replace(REGEX_ESCAPE_RE, '\\\\$&'));\n return new RegExp(escaped.join('|'), 'i');\n}\n\n// Pre-built once at module load — avoids `new RegExp()` on every `maskSecrets()`\n// or `maskReplacer()` call when defaults are used (the common case).\nconst DEFAULT_SECRET_RE = buildSecretRe(DEFAULT_SECRET_PATTERNS);\nconst DEFAULT_RESOLVED: ResolvedOptions = {\n secretRe: DEFAULT_SECRET_RE,\n mask: DEFAULT_MASK,\n deep: true,\n};\n\nfunction resolveOptions(options: MaskSecretsOptions): ResolvedOptions {\n // Fast path: defaults — return shared singleton, zero allocation.\n if (\n options.patterns === undefined &&\n options.mask === undefined &&\n options.deep === undefined\n ) {\n return DEFAULT_RESOLVED;\n }\n const { patterns = DEFAULT_SECRET_PATTERNS, mask = DEFAULT_MASK, deep = true } = options;\n return {\n secretRe: patterns === DEFAULT_SECRET_PATTERNS ? DEFAULT_SECRET_RE : buildSecretRe(patterns),\n mask,\n deep,\n };\n}\n\nfunction isSecretKey(key: string, secretRe: RegExp): boolean {\n return secretRe.test(key);\n}\n\nfunction maskUrlCredentials(url: string, mask: string): string {\n try {\n const parsed = new URL(url);\n if (parsed.password) {\n parsed.password = mask;\n }\n if (parsed.username && parsed.password) {\n parsed.username = mask;\n }\n return parsed.toString();\n } catch {\n return url;\n }\n}\n\nfunction maskString(str: string, mask: string): string {\n // Cheap pre-checks before the O(n) indexOf scan and URL parse:\n // 1. Too short to be `http(s)://x:y@h` (10 chars min).\n // 2. First char must be 'h' (104) — bails on the vast majority of strings\n // with a single charCodeAt, no allocation.\n // 3. Then verify http/https prefix.\n // 4. Only then scan for '@'.\n if (str.length < 10) return str;\n if (str.charCodeAt(0) !== 104) return str;\n if (!str.startsWith('http://') && !str.startsWith('https://')) return str;\n if (str.indexOf('@') === -1) return str;\n return maskUrlCredentials(str, mask);\n}\n\nfunction maskInternal(obj: unknown, opts: ResolvedOptions): unknown {\n if (obj === null || obj === undefined) return obj;\n\n const t = typeof obj;\n if (t === 'string') return maskString(obj as string, opts.mask);\n if (t !== 'object') return obj;\n\n if (Array.isArray(obj)) {\n return opts.deep ? obj.map((item) => maskInternal(item, opts)) : obj;\n }\n\n const { secretRe, mask, deep } = opts;\n const result: Record<string | symbol, unknown> = {};\n\n // Symbol-keyed properties are preserved on the clone; JSON.stringify drops\n // them, but external callers may pass objects where these matter.\n const syms = Object.getOwnPropertySymbols(obj);\n for (let i = 0; i < syms.length; i++) {\n const sym = syms[i];\n result[sym] = (obj as Record<symbol, unknown>)[sym];\n }\n\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n if (isSecretKey(key, secretRe)) {\n result[key] = mask;\n continue;\n }\n const vt = typeof value;\n if (vt === 'string') {\n result[key] = maskString(value as string, mask);\n } else if (deep && vt === 'object' && value !== null) {\n result[key] = maskInternal(value, opts);\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\nexport function maskSecrets(\n obj: unknown,\n options: MaskSecretsOptions = {},\n): unknown {\n return maskInternal(obj, resolveOptions(options));\n}\n\nexport function createMasker(options: MaskSecretsOptions = {}) {\n const opts = resolveOptions(options);\n return (obj: unknown): unknown => maskInternal(obj, opts);\n}\n\nexport type MaskReplacer = (this: unknown, key: string, value: unknown) => unknown;\n\n/**\n * Returns a `JSON.stringify` replacer that masks secret-named keys and URL\n * credentials in-place during serialization. Unlike `maskSecrets`/`createMasker`\n * it allocates no intermediate object — the recursion is driven by `JSON.stringify`.\n *\n * Use it as `JSON.stringify(value, maskReplacer())` or\n * `JSON.stringify(value, maskReplacer(), 2)`.\n */\nexport function maskReplacer(options: MaskSecretsOptions = {}): MaskReplacer {\n const { secretRe, mask } = resolveOptions(options);\n return function (key, value) {\n // Hot path inlined: avoid extra function calls per key/value pair.\n // - Root call has key === '' (skip key check, fall through to string check).\n // - Most values are non-strings (objects/numbers/etc) — bail early.\n if (key !== '' && secretRe.test(key)) return mask;\n if (typeof value !== 'string') return value;\n // maskString hot path inlined for the same reason.\n if (value.length < 10) return value;\n if (value.charCodeAt(0) !== 104) return value;\n if (!value.startsWith('http://') && !value.startsWith('https://')) return value;\n if (value.indexOf('@') === -1) return value;\n return maskUrlCredentials(value, mask);\n };\n}\n","import { Writable } from 'node:stream';\nimport type WebSocket from 'ws';\nimport type { RelayConfig } from '../types.js';\nimport { maskReplacer, type MaskReplacer } from '../utils/mask-secrets.js';\n\n// Duplicated here because worker thread cannot import from main bundle\nconst LOG_LEVELS: Record<string, number> = {\n off: -1,\n error: 0,\n warn: 1,\n info: 2,\n http: 3,\n verbose: 4,\n debug: 5,\n silly: 6,\n};\n\nconst PINO_LEVEL_TO_NAME: Record<number, string> = {\n 60: 'error', // fatal → error\n 50: 'error',\n 40: 'warn',\n 30: 'info',\n 25: 'http',\n 20: 'debug',\n 10: 'silly',\n};\n\nfunction getLevelName(pinoLevel: number): string {\n if (pinoLevel >= 50) return 'error';\n if (pinoLevel >= 40) return 'warn';\n if (pinoLevel >= 30) return 'info';\n if (pinoLevel >= 25) return 'http';\n if (pinoLevel >= 20) return 'debug';\n return 'silly';\n}\n\ninterface ParsedLog {\n level: number;\n time: number;\n msg: string;\n context?: string;\n [key: string]: unknown;\n}\n\ninterface RelayRule {\n level?: string;\n context?: string;\n match?: Record<string, unknown>;\n}\n\ninterface ServerConfig {\n enabled: boolean;\n rules?: RelayRule[];\n wsUrl?: string;\n}\n\ntype RelayState = 'POLLING' | 'CONNECTING' | 'STREAMING' | 'RECONNECTING';\n\nconst DEFAULTS = {\n pollInterval: 30_000,\n bufferSize: 1000,\n reconnectDelay: 1_000,\n maxReconnectDelay: 30_000,\n};\n\nclass RingBuffer<T> {\n private items: T[];\n private head = 0;\n private count = 0;\n\n constructor(private capacity: number) {\n this.items = new Array(capacity);\n }\n\n push(item: T): void {\n this.items[(this.head + this.count) % this.capacity] = item;\n if (this.count < this.capacity) {\n this.count++;\n } else {\n this.head = (this.head + 1) % this.capacity;\n }\n }\n\n drain(): T[] {\n const result: T[] = [];\n for (let i = 0; i < this.count; i++) {\n result.push(this.items[(this.head + i) % this.capacity]);\n }\n this.head = 0;\n this.count = 0;\n return result;\n }\n\n get size(): number {\n return this.count;\n }\n}\n\nfunction matchesRules(log: ParsedLog, rules: RelayRule[]): boolean {\n if (!rules || rules.length === 0) return true;\n\n return rules.some((rule) => {\n if (rule.level) {\n const logLevelNum = LOG_LEVELS[getLevelName(log.level)] ?? 6;\n const ruleLevelNum = LOG_LEVELS[rule.level] ?? 6;\n if (logLevelNum > ruleLevelNum) return false;\n }\n if (rule.context && log.context !== rule.context) return false;\n if (rule.match) {\n for (const [key, value] of Object.entries(rule.match)) {\n if (log[key] !== value) return false;\n }\n }\n return true;\n });\n}\n\n/**\n * Returns true iff `wsUrl`'s origin matches `apiUrl`'s origin OR its hostname\n * is in the explicit allowlist. Defends against a compromised /api/relay/config\n * endpoint pointing log streams at an attacker-controlled WebSocket.\n *\n * @internal Exported for tests.\n */\nexport function isWsUrlAllowed(\n wsUrl: string,\n apiUrl: string,\n allowedHosts: string[] | undefined,\n): boolean {\n let parsed: URL;\n let api: URL;\n try {\n parsed = new URL(wsUrl);\n api = new URL(apiUrl);\n } catch {\n return false;\n }\n if (parsed.hostname === api.hostname) return true;\n if (allowedHosts && allowedHosts.includes(parsed.hostname)) return true;\n return false;\n}\n\nexport default function (opts: RelayConfig): Writable {\n const pollInterval = opts.pollInterval ?? DEFAULTS.pollInterval;\n const bufferSize = opts.bufferSize ?? DEFAULTS.bufferSize;\n const reconnectDelay = opts.reconnectDelay ?? DEFAULTS.reconnectDelay;\n const maxReconnectDelay = opts.maxReconnectDelay ?? DEFAULTS.maxReconnectDelay;\n\n // Build the masking replacer once. Undefined when masking disabled.\n let replacer: MaskReplacer | undefined;\n if (opts.maskSecrets) {\n const maskOpts = opts.maskSecrets === true ? {} : opts.maskSecrets;\n replacer = maskReplacer(maskOpts);\n }\n\n let state: RelayState = 'POLLING';\n let serverConfig: ServerConfig | null = null;\n let ws: import('ws').WebSocket | null = null;\n let pollTimer: NodeJS.Timeout | null = null;\n let reconnectTimer: NodeJS.Timeout | null = null;\n let reconnectAttempt = 0;\n let closed = false;\n let announcedStreamingFor: string | null = null;\n\n const buffer = new RingBuffer<string>(bufferSize);\n\n function maskLine(line: string): string {\n if (!replacer) return line;\n try {\n const parsed = JSON.parse(line);\n return JSON.stringify(parsed, replacer);\n } catch {\n // Non-JSON or malformed — forward as-is rather than drop.\n return line;\n }\n }\n\n // Lazy-load ws to fail gracefully if not installed\n let WebSocketClass: (new (url: string, opts?: object) => WebSocket) | null = null;\n\n async function loadWebSocket(): Promise<new (url: string, opts?: object) => WebSocket> {\n if (WebSocketClass) return WebSocketClass;\n try {\n const wsModule = await import('ws');\n WebSocketClass = wsModule.default as unknown as new (url: string, opts?: object) => WebSocket;\n return WebSocketClass;\n } catch {\n throw new Error(\n '@rawnodes/logger relay transport requires the \"ws\" package. Install it: npm install ws',\n );\n }\n }\n\n function sendLine(line: string): void {\n if (ws && ws.readyState === 1 /* OPEN */) {\n // Backpressure check: if too much buffered in WS, drop\n if (ws.bufferedAmount > 1024 * 1024) return;\n ws.send(maskLine(line));\n }\n }\n\n function flushBuffer(): void {\n const lines = buffer.drain();\n for (const line of lines) {\n sendLine(line);\n }\n }\n\n async function openWebSocket(url: string): Promise<void> {\n if (closed) return;\n\n state = 'CONNECTING';\n reconnectAttempt = 0;\n\n try {\n const WS = await loadWebSocket();\n const socket = new WS(url, {\n headers: { authorization: `Bearer ${opts.token}` },\n });\n ws = socket;\n\n socket.on('open', () => {\n state = 'STREAMING';\n reconnectAttempt = 0;\n // Audit trail: emit once per distinct wsUrl so logs reflect when —\n // and where — remote streaming was actually engaged.\n if (announcedStreamingFor !== url) {\n announcedStreamingFor = url;\n process.stderr.write(\n `[RelayTransport] streaming logs to ${url}` +\n (replacer ? ' (maskSecrets: on)' : '') + '\\n',\n );\n }\n flushBuffer();\n });\n\n socket.on('close', () => {\n if (!closed) scheduleReconnect(url);\n });\n\n socket.on('error', (err) => {\n console.error('[RelayTransport] WebSocket error:', err.message);\n socket.close();\n });\n\n // Server can push updated rules over the same connection\n socket.on('message', (data) => {\n try {\n const msg = JSON.parse(data.toString());\n if (msg.type === 'config' && msg.rules) {\n if (serverConfig) serverConfig.rules = msg.rules;\n }\n if (msg.type === 'config' && msg.enabled === false) {\n closeWebSocket();\n serverConfig = { enabled: false };\n }\n } catch {\n // ignore non-JSON messages\n }\n });\n } catch (err) {\n console.error('[RelayTransport]', (err as Error).message);\n if (!closed) scheduleReconnect(url);\n }\n }\n\n function scheduleReconnect(url: string): void {\n if (closed) return;\n state = 'RECONNECTING';\n const delay = Math.min(reconnectDelay * Math.pow(2, reconnectAttempt), maxReconnectDelay);\n reconnectAttempt++;\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n void openWebSocket(url);\n }, delay);\n }\n\n function closeWebSocket(): void {\n if (ws) {\n ws.removeAllListeners();\n if (ws.readyState === 0 /* CONNECTING */ || ws.readyState === 1 /* OPEN */) {\n ws.close();\n }\n ws = null;\n }\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n state = 'POLLING';\n }\n\n async function poll(): Promise<void> {\n if (closed) return;\n\n try {\n const res = await fetch(`${opts.apiUrl}/api/relay/config`, {\n headers: { authorization: `Bearer ${opts.token}` },\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!res.ok) return;\n\n const config = (await res.json()) as ServerConfig;\n serverConfig = config;\n\n if (config.enabled && config.wsUrl) {\n if (!isWsUrlAllowed(config.wsUrl, opts.apiUrl, opts.allowedWsHosts)) {\n // Server tried to redirect logs to a host we don't trust. Refuse and\n // keep polling — surface to stderr so an operator notices.\n process.stderr.write(\n `[RelayTransport] refusing wsUrl \"${config.wsUrl}\" — origin does not match apiUrl \"${opts.apiUrl}\" and host is not in allowedWsHosts. Set RelayConfig.allowedWsHosts to authorize alternate hosts.\\n`,\n );\n return;\n }\n if (state === 'POLLING') {\n void openWebSocket(config.wsUrl);\n }\n } else {\n if (state !== 'POLLING') {\n closeWebSocket();\n }\n }\n } catch {\n // Server unreachable — keep polling, don't disrupt app\n }\n }\n\n // Start polling\n void poll();\n pollTimer = setInterval(() => void poll(), pollInterval);\n\n const stream = new Writable({\n write(chunk: Buffer, _encoding: string, callback: () => void): void {\n if (closed) {\n callback();\n return;\n }\n\n const line = chunk.toString().trim();\n if (!line) {\n callback();\n return;\n }\n\n // When disabled — drop immediately, zero overhead\n if (state === 'POLLING' || !serverConfig?.enabled) {\n callback();\n return;\n }\n\n // Filter by server rules\n if (serverConfig.rules && serverConfig.rules.length > 0) {\n try {\n const log = JSON.parse(line) as ParsedLog;\n if (!matchesRules(log, serverConfig.rules)) {\n callback();\n return;\n }\n } catch {\n callback();\n return;\n }\n }\n\n if (state === 'STREAMING') {\n sendLine(line);\n } else if (state === 'RECONNECTING' || state === 'CONNECTING') {\n buffer.push(line);\n }\n\n callback();\n },\n\n final(callback: () => void): void {\n closed = true;\n\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n\n closeWebSocket();\n callback();\n },\n\n destroy(error: Error | null, callback: (error: Error | null) => void): void {\n closed = true;\n\n if (pollTimer) {\n clearInterval(pollTimer);\n pollTimer = null;\n }\n\n closeWebSocket();\n callback(error);\n },\n });\n\n return stream;\n}\n"]}