first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
This client is based upon the great work done in this project:
|
||||
https://github.com/paulgrove/node-syslog-client/tree/master
|
||||
|
||||
It was partially built with Claude Code. However, many of the tests were verified and / or added manually.
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { nodeConfig } from '@n8n/eslint-config/node';
|
||||
|
||||
export default defineConfig(
|
||||
nodeConfig,
|
||||
{
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.config.ts'],
|
||||
rules: {
|
||||
'n8n-local-rules/no-untyped-config-class-field': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@n8n/syslog-client",
|
||||
"version": "1.2.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"format": "biome format --write src test",
|
||||
"format:check": "biome ci src test",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"get-port": "^7.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
import * as dgram from 'dgram';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as net from 'net';
|
||||
import * as os from 'os';
|
||||
import * as tls from 'tls';
|
||||
|
||||
import { Facility, Severity, Transport } from './constants';
|
||||
import {
|
||||
ConnectionError,
|
||||
SyslogClientError,
|
||||
TimeoutError,
|
||||
TransportError,
|
||||
ValidationError,
|
||||
} from './errors';
|
||||
import { clientOptionsSchema, logOptionsSchema } from './schemas';
|
||||
import type {
|
||||
ClientOptions,
|
||||
DateFormatter,
|
||||
LogOptions,
|
||||
ResolvedLogOptions,
|
||||
SyslogCallback,
|
||||
TransportConnection,
|
||||
} from './types';
|
||||
import { buildFormattedMessage, defaultDateFormatter, isIPv6 } from './utils';
|
||||
|
||||
/**
|
||||
* Syslog client supporting UDP, TCP, TLS, and Unix socket transports.
|
||||
* Supports both RFC 3164 and RFC 5424 formats.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new SyslogClient('192.168.1.1', {
|
||||
* transport: Transport.Tcp,
|
||||
* facility: Facility.Local0,
|
||||
* });
|
||||
*
|
||||
* // Callback API
|
||||
* client.log('Test message', (error) => {
|
||||
* if (error) console.error(error);
|
||||
* });
|
||||
*
|
||||
* // Promise API (when no callback provided)
|
||||
* await client.log('Test message');
|
||||
* await client.log('Test message', { severity: Severity.Error });
|
||||
*
|
||||
* client.close();
|
||||
* ```
|
||||
*/
|
||||
export class SyslogClient extends EventEmitter {
|
||||
// Public configuration properties
|
||||
readonly target: string;
|
||||
readonly syslogHostname: string;
|
||||
readonly port: number;
|
||||
readonly tcpTimeout: number;
|
||||
readonly facility: Facility;
|
||||
readonly severity: Severity;
|
||||
readonly rfc3164: boolean;
|
||||
readonly appName: string;
|
||||
readonly dateFormatter: DateFormatter;
|
||||
readonly udpBindAddress?: string;
|
||||
readonly transport: Transport;
|
||||
readonly tlsCA?: string | string[] | Buffer | Buffer[];
|
||||
|
||||
// Private state
|
||||
private transport_?: TransportConnection;
|
||||
private connecting = false;
|
||||
private getTransportRequests: Array<
|
||||
(error: Error | null, transport?: TransportConnection) => void
|
||||
> = [];
|
||||
|
||||
/**
|
||||
* Create a new syslog client.
|
||||
*
|
||||
* @param target - Target host/path (IP address, hostname, or Unix socket path)
|
||||
* @param options - Client configuration options
|
||||
* @throws {ValidationError} If options validation fails
|
||||
*/
|
||||
constructor(target?: string, options?: ClientOptions) {
|
||||
super();
|
||||
|
||||
this.target = target ?? '127.0.0.1';
|
||||
|
||||
const validationResult = clientOptionsSchema.safeParse(options ?? {});
|
||||
if (!validationResult.success) {
|
||||
throw ValidationError.fromZod('Invalid client options', validationResult.error.errors);
|
||||
}
|
||||
|
||||
const opts = validationResult.data;
|
||||
|
||||
// Initialize properties with defaults
|
||||
this.syslogHostname = opts.syslogHostname ?? os.hostname();
|
||||
this.port = opts.port ?? 514;
|
||||
this.tcpTimeout = opts.tcpTimeout ?? 10000;
|
||||
|
||||
// BUG FIX: Original code has incorrect logic (typeof !== "number" || default)
|
||||
// Should be: typeof === "number" ? value : default
|
||||
this.facility = typeof opts.facility === 'number' ? opts.facility : Facility.Local0;
|
||||
this.severity = typeof opts.severity === 'number' ? opts.severity : Severity.Informational;
|
||||
|
||||
this.rfc3164 = opts.rfc3164 ?? true;
|
||||
this.appName = opts.appName ?? process.title.substring(process.title.lastIndexOf('/') + 1, 48);
|
||||
this.dateFormatter = opts.dateFormatter ?? defaultDateFormatter;
|
||||
this.udpBindAddress = opts.udpBindAddress;
|
||||
this.transport = opts.transport ?? Transport.Udp;
|
||||
this.tlsCA = opts.tlsCA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to syslog.
|
||||
* Supports both callback and promise-based API.
|
||||
*
|
||||
* @param message - Message to log
|
||||
* @param options - Optional log options or callback
|
||||
* @param errorCb - Optional callback
|
||||
* @returns Promise<void> if no callback provided, otherwise void
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Callback API
|
||||
* client.log('Test message', (error) => {
|
||||
* if (error) console.error(error);
|
||||
* });
|
||||
*
|
||||
* // Promise API
|
||||
* await client.log('Test message');
|
||||
* await client.log('Test message', { severity: Severity.Error });
|
||||
* ```
|
||||
*/
|
||||
log(
|
||||
message: string,
|
||||
options?: LogOptions | SyslogCallback,
|
||||
errorCb?: SyslogCallback,
|
||||
): Promise<void> | void {
|
||||
// Parse arguments
|
||||
let opts: LogOptions = {};
|
||||
let logCallback: SyslogCallback | undefined;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
logCallback = options;
|
||||
} else if (typeof options === 'object') {
|
||||
opts = options;
|
||||
logCallback = errorCb;
|
||||
}
|
||||
|
||||
// Promise mode: no callback provided
|
||||
if (!logCallback) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.logInternal(message, opts, (error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Callback mode
|
||||
this.logInternal(message, opts, logCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal log implementation using callbacks.
|
||||
*/
|
||||
private logInternal(message: string, options: LogOptions, errorCb: SyslogCallback): void {
|
||||
// Validate options
|
||||
const validationResult = logOptionsSchema.safeParse(options);
|
||||
if (!validationResult.success) {
|
||||
errorCb(ValidationError.fromZod('Invalid log options', validationResult.error.errors));
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve options with defaults
|
||||
const resolvedOptions: ResolvedLogOptions = {
|
||||
facility: options.facility ?? this.facility,
|
||||
severity: options.severity ?? this.severity,
|
||||
rfc3164: options.rfc3164 ?? this.rfc3164,
|
||||
appName: options.appName ?? this.appName,
|
||||
syslogHostname: options.syslogHostname ?? this.syslogHostname,
|
||||
timestamp: options.timestamp,
|
||||
msgid: options.msgid,
|
||||
};
|
||||
|
||||
// Build formatted message
|
||||
const formattedMessage = buildFormattedMessage(message, resolvedOptions, this.dateFormatter);
|
||||
|
||||
// Get transport and send
|
||||
this.getTransport((error, transport) => {
|
||||
if (error || !transport) {
|
||||
errorCb(error ?? new ConnectionError('Failed to get transport'));
|
||||
return;
|
||||
}
|
||||
this.sendMessage(transport, formattedMessage, errorCb);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message via transport.
|
||||
*/
|
||||
private sendMessage(
|
||||
transport: TransportConnection,
|
||||
message: Buffer,
|
||||
completionCb: SyslogCallback,
|
||||
): void {
|
||||
try {
|
||||
if (this.isStreamSocket(transport)) {
|
||||
// TCP/TLS/Unix: use write
|
||||
transport.write(message, (error) => {
|
||||
if (error) {
|
||||
completionCb(new TransportError('Write failed', this.getTransportName(), error));
|
||||
} else {
|
||||
completionCb();
|
||||
}
|
||||
});
|
||||
} else if (this.isUdpSocket(transport)) {
|
||||
// UDP: use send
|
||||
transport.send(message, 0, message.length, this.port, this.target, (error) => {
|
||||
if (error) {
|
||||
completionCb(new TransportError('Send failed', 'UDP', error));
|
||||
} else {
|
||||
completionCb();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
completionCb(new SyslogClientError(`Unknown transport: ${this.transport}`));
|
||||
}
|
||||
} catch (error) {
|
||||
this.onError(this.normalizeError(error));
|
||||
completionCb(this.normalizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create transport connection.
|
||||
*/
|
||||
private getTransport(
|
||||
completionCb: (error: Error | null, transport?: TransportConnection) => void,
|
||||
): void {
|
||||
// Return existing transport
|
||||
if (this.transport_) {
|
||||
completionCb(null, this.transport_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue request
|
||||
this.getTransportRequests.push(completionCb);
|
||||
|
||||
// Already connecting, wait for result
|
||||
if (this.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connecting = true;
|
||||
|
||||
// Create transport and notify all waiting requests
|
||||
const notifyAllWaitingRequests = (error: Error | null, transport?: TransportConnection) => {
|
||||
// Drain queue: notify all waiting callbacks
|
||||
while (this.getTransportRequests.length > 0) {
|
||||
const listenerCb = this.getTransportRequests.shift();
|
||||
if (listenerCb) listenerCb(error, transport);
|
||||
}
|
||||
this.connecting = false;
|
||||
};
|
||||
|
||||
// Create appropriate transport
|
||||
if (this.transport === Transport.Udp) {
|
||||
this.createUdpTransport(notifyAllWaitingRequests);
|
||||
} else if (this.transport === Transport.Tcp || this.transport === Transport.Unix) {
|
||||
this.createTcpTransport(notifyAllWaitingRequests);
|
||||
} else if (this.transport === Transport.Tls) {
|
||||
this.createTlsTransport(notifyAllWaitingRequests);
|
||||
} else {
|
||||
notifyAllWaitingRequests(
|
||||
new SyslogClientError(`Unknown transport: ${this.getTransportName()}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TCP or Unix socket transport.
|
||||
*/
|
||||
private createTcpTransport(
|
||||
completionCb: (error: Error | null, transport?: TransportConnection) => void,
|
||||
): void {
|
||||
const options =
|
||||
this.transport === Transport.Unix
|
||||
? { path: this.target }
|
||||
: {
|
||||
host: this.target,
|
||||
port: this.port,
|
||||
family: isIPv6(this.target) ? 6 : 4,
|
||||
};
|
||||
|
||||
let transport: net.Socket;
|
||||
try {
|
||||
transport = net.createConnection(options, () =>
|
||||
this.onSocketConnected(transport, completionCb),
|
||||
);
|
||||
} catch (error) {
|
||||
completionCb(
|
||||
new ConnectionError('Failed to create TCP connection', this.normalizeError(error)),
|
||||
);
|
||||
this.onError(this.normalizeError(error));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupSocketHandlers(transport, completionCb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create TLS transport.
|
||||
*/
|
||||
private createTlsTransport(
|
||||
completionCb: (error: Error | null, transport?: TransportConnection) => void,
|
||||
): void {
|
||||
const options: tls.ConnectionOptions = {
|
||||
host: this.target,
|
||||
port: this.port,
|
||||
ca: this.tlsCA,
|
||||
minVersion: 'TLSv1.2',
|
||||
};
|
||||
|
||||
let transport: tls.TLSSocket;
|
||||
try {
|
||||
transport = tls.connect(options, () => this.onSocketConnected(transport, completionCb));
|
||||
} catch (error) {
|
||||
completionCb(
|
||||
new ConnectionError('Failed to create TLS connection', this.normalizeError(error)),
|
||||
);
|
||||
this.onError(this.normalizeError(error));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupSocketHandlers(transport, completionCb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers for stream-based transports (TCP/TLS/Unix).
|
||||
*/
|
||||
private setupSocketHandlers(
|
||||
socket: net.Socket | tls.TLSSocket,
|
||||
completionCb: (error: Error | null, transport?: TransportConnection) => void,
|
||||
): void {
|
||||
// Timeout handler
|
||||
socket.setTimeout(this.tcpTimeout, () => {
|
||||
const error = new TimeoutError();
|
||||
socket.destroy();
|
||||
this.emit('error', error);
|
||||
completionCb(error);
|
||||
});
|
||||
|
||||
// Error handler
|
||||
socket.on('error', (socketError: Error) => {
|
||||
socket.destroy();
|
||||
const error = new ConnectionError('Transport error', socketError);
|
||||
this.onError(socketError);
|
||||
completionCb(error);
|
||||
});
|
||||
|
||||
// Close handler
|
||||
socket.on('close', this.onClose.bind(this));
|
||||
socket.unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful socket connection.
|
||||
*/
|
||||
private onSocketConnected(
|
||||
socket: net.Socket | tls.TLSSocket,
|
||||
completionCb: (error: Error | null, transport?: TransportConnection) => void,
|
||||
): void {
|
||||
this.transport_ = socket;
|
||||
socket.setTimeout(0); // Clear connection timeout
|
||||
completionCb(null, this.transport_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UDP transport.
|
||||
*/
|
||||
private createUdpTransport(
|
||||
completionCb: (error: Error | null, transport?: TransportConnection) => void,
|
||||
): void {
|
||||
try {
|
||||
const family = isIPv6(this.target) ? 6 : 4;
|
||||
this.transport_ = dgram.createSocket(`udp${family}` as dgram.SocketType);
|
||||
|
||||
// Bind to specific address if specified
|
||||
if (this.udpBindAddress) {
|
||||
this.transport_.bind({ address: this.udpBindAddress });
|
||||
}
|
||||
|
||||
// Setup event handlers
|
||||
this.transport_.on('close', this.onClose.bind(this));
|
||||
this.transport_.on('error', (transportError) => {
|
||||
const error = new ConnectionError('UDP socket error', transportError);
|
||||
this.onError(error);
|
||||
completionCb(error);
|
||||
});
|
||||
|
||||
// Unref to not block process exit
|
||||
this.transport_.unref();
|
||||
|
||||
completionCb(null, this.transport_);
|
||||
} catch (transportError) {
|
||||
if (this.transport_ && this.isUdpSocket(this.transport_)) {
|
||||
try {
|
||||
this.transport_.close();
|
||||
} catch {
|
||||
// Ignore cleanup error
|
||||
}
|
||||
}
|
||||
const error = this.normalizeError(transportError);
|
||||
this.onError(error);
|
||||
completionCb(new ConnectionError('Failed to create UDP socket', error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the client and destroy the transport.
|
||||
*
|
||||
* @returns this for chaining
|
||||
*/
|
||||
close(): this {
|
||||
if (this.transport_) {
|
||||
if (this.isStreamSocket(this.transport_)) {
|
||||
this.transport_.destroy();
|
||||
} else if (this.isUdpSocket(this.transport_)) {
|
||||
this.transport_.close();
|
||||
}
|
||||
this.transport_ = undefined;
|
||||
} else {
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle close event.
|
||||
*/
|
||||
private onClose(): this {
|
||||
if (this.transport_) {
|
||||
if ('destroy' in this.transport_) {
|
||||
this.transport_.destroy();
|
||||
}
|
||||
this.transport_ = undefined;
|
||||
}
|
||||
|
||||
this.emit('close');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error event.
|
||||
*/
|
||||
private onError(error: Error): this {
|
||||
if (this.transport_) {
|
||||
if ('destroy' in this.transport_) {
|
||||
this.transport_.destroy();
|
||||
}
|
||||
this.transport_ = undefined;
|
||||
}
|
||||
|
||||
this.emit('error', error);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if transport is a stream socket (TCP/TLS/Unix).
|
||||
*/
|
||||
private isStreamSocket(transport: TransportConnection): transport is net.Socket | tls.TLSSocket {
|
||||
return 'write' in transport && typeof transport.write === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if transport is a UDP socket.
|
||||
*/
|
||||
private isUdpSocket(transport: TransportConnection): transport is dgram.Socket {
|
||||
return 'send' in transport && typeof transport.send === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transport name as string.
|
||||
* Required because const enums don't have reverse mapping at runtime.
|
||||
*/
|
||||
private getTransportName(): string {
|
||||
switch (this.transport) {
|
||||
case Transport.Tcp:
|
||||
return 'TCP';
|
||||
case Transport.Udp:
|
||||
return 'UDP';
|
||||
case Transport.Tls:
|
||||
return 'TLS';
|
||||
case Transport.Unix:
|
||||
return 'Unix';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize any error to an Error instance.
|
||||
*/
|
||||
private normalizeError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* We want the runtime overhead here */
|
||||
|
||||
/**
|
||||
* Transport protocols supported by the syslog client.
|
||||
*/
|
||||
export enum Transport {
|
||||
Tcp = 1,
|
||||
Udp = 2,
|
||||
Tls = 3,
|
||||
Unix = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* Syslog facility codes as defined in RFC 5424.
|
||||
*/
|
||||
export enum Facility {
|
||||
Kernel = 0,
|
||||
User = 1,
|
||||
Mail = 2,
|
||||
System = 3,
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
||||
Daemon = 3,
|
||||
Auth = 4,
|
||||
Syslog = 5,
|
||||
Lpr = 6,
|
||||
News = 7,
|
||||
Uucp = 8,
|
||||
Cron = 9,
|
||||
Authpriv = 10,
|
||||
Ftp = 11,
|
||||
Audit = 13,
|
||||
Alert = 14,
|
||||
Local0 = 16,
|
||||
Local1 = 17,
|
||||
Local2 = 18,
|
||||
Local3 = 19,
|
||||
Local4 = 20,
|
||||
Local5 = 21,
|
||||
Local6 = 22,
|
||||
Local7 = 23,
|
||||
}
|
||||
|
||||
/**
|
||||
* Syslog severity levels as defined in RFC 5424.
|
||||
*/
|
||||
export enum Severity {
|
||||
Emergency = 0,
|
||||
Alert = 1,
|
||||
Critical = 2,
|
||||
Error = 3,
|
||||
Warning = 4,
|
||||
Notice = 5,
|
||||
Informational = 6,
|
||||
Debug = 7,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { ZodIssue } from 'zod';
|
||||
|
||||
/**
|
||||
* Base error class for all syslog client errors.
|
||||
* Extends native Error with additional context.
|
||||
*/
|
||||
export class SyslogClientError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code?: string,
|
||||
readonly cause?: Error,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SyslogClientError';
|
||||
|
||||
// Maintain proper prototype chain for instanceof checks
|
||||
Object.setPrototypeOf(this, SyslogClientError.prototype);
|
||||
|
||||
// Capture stack trace, excluding constructor call from it
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when client options validation fails.
|
||||
*/
|
||||
export class ValidationError extends SyslogClientError {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly validationErrors: Array<{ path: string; message: string }>,
|
||||
) {
|
||||
super(message, 'VALIDATION_ERROR');
|
||||
this.name = 'ValidationError';
|
||||
Object.setPrototypeOf(this, ValidationError.prototype);
|
||||
}
|
||||
|
||||
static fromZod(message: string, zodErrors: ZodIssue[]) {
|
||||
const errors = zodErrors.map((zodError) => ({
|
||||
path: zodError.path.join('.'),
|
||||
message: zodError.message,
|
||||
}));
|
||||
|
||||
return new ValidationError(message, errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when transport connection fails.
|
||||
*/
|
||||
export class ConnectionError extends SyslogClientError {
|
||||
constructor(message: string, cause?: Error) {
|
||||
super(message, 'CONNECTION_ERROR', cause);
|
||||
this.name = 'ConnectionError';
|
||||
Object.setPrototypeOf(this, ConnectionError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when transport operations fail (send/write).
|
||||
*/
|
||||
export class TransportError extends SyslogClientError {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly transportType: string,
|
||||
cause?: Error,
|
||||
) {
|
||||
super(message, 'TRANSPORT_ERROR', cause);
|
||||
this.name = 'TransportError';
|
||||
Object.setPrototypeOf(this, TransportError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when timeout occurs.
|
||||
*/
|
||||
export class TimeoutError extends SyslogClientError {
|
||||
constructor(message: string = 'Connection timed out') {
|
||||
super(message, 'TIMEOUT_ERROR');
|
||||
this.name = 'TimeoutError';
|
||||
Object.setPrototypeOf(this, TimeoutError.prototype);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { SyslogClient } from './client';
|
||||
import type { ClientOptions } from './types';
|
||||
|
||||
/**
|
||||
* Factory function to create a syslog client.
|
||||
* Provided for backward compatibility with original API.
|
||||
*
|
||||
* @param target - Target host/path (IP address, hostname, or Unix socket path)
|
||||
* @param options - Client configuration options
|
||||
* @returns New SyslogClient instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createClient, Transport } from '@n8n/syslog-client';
|
||||
*
|
||||
* const client = createClient('192.168.1.1', {
|
||||
* transport: Transport.Tcp,
|
||||
* port: 514,
|
||||
* });
|
||||
*
|
||||
* await client.log('Test message');
|
||||
* client.close();
|
||||
* ```
|
||||
*/
|
||||
export function createClient(target?: string, options?: ClientOptions): SyslogClient {
|
||||
return new SyslogClient(target, options);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @n8n/syslog-client
|
||||
*
|
||||
* A syslog client for Node.js supporting UDP, TCP, TLS, and Unix socket transports.
|
||||
* Supports both RFC 3164 and RFC 5424 syslog message formats.
|
||||
*
|
||||
* Based upon the great work done in:
|
||||
* https://github.com/paulgrove/node-syslog-client
|
||||
*/
|
||||
|
||||
export { SyslogClient } from './client';
|
||||
export { createClient } from './factory';
|
||||
export { Facility, Severity, Transport } from './constants';
|
||||
export type { ClientOptions, DateFormatter, LogOptions, SyslogCallback } from './types';
|
||||
export {
|
||||
ConnectionError,
|
||||
SyslogClientError,
|
||||
TimeoutError,
|
||||
TransportError,
|
||||
ValidationError,
|
||||
} from './errors';
|
||||
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Facility, Severity, Transport } from './constants';
|
||||
|
||||
/**
|
||||
* Helper to dynamically create Zod schema for enum values.
|
||||
* Filters out string keys that regular enums create at runtime.
|
||||
*/
|
||||
const createEnumSchema = (enumObject: object, name: string) => {
|
||||
const values = Object.values(enumObject).filter((val) => typeof val === 'number');
|
||||
return z.number().refine((val) => values.includes(val), {
|
||||
message: `Invalid ${name} value. Must be one of: ${values.join(', ')}`,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Zod schema for validating ClientOptions.
|
||||
*/
|
||||
export const clientOptionsSchema = z.object({
|
||||
syslogHostname: z.string().optional(),
|
||||
port: z.number().int().positive().max(65535).optional(),
|
||||
tcpTimeout: z.number().int().positive().optional(),
|
||||
facility: createEnumSchema(Facility, 'facility').optional(),
|
||||
severity: createEnumSchema(Severity, 'severity').optional(),
|
||||
rfc3164: z.boolean().optional(),
|
||||
appName: z.string().max(48).optional(), // RFC 5424 limit
|
||||
dateFormatter: z.function().args(z.date()).returns(z.string()).optional(),
|
||||
udpBindAddress: z.string().ip().optional(),
|
||||
transport: createEnumSchema(Transport, 'transport').optional(),
|
||||
tlsCA: z
|
||||
.union([z.string(), z.array(z.string()), z.instanceof(Buffer), z.array(z.instanceof(Buffer))])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zod schema for validating LogOptions.
|
||||
*/
|
||||
export const logOptionsSchema = z.object({
|
||||
facility: createEnumSchema(Facility, 'facility').optional(),
|
||||
severity: createEnumSchema(Severity, 'severity').optional(),
|
||||
rfc3164: z.boolean().optional(),
|
||||
appName: z.string().max(48).optional(),
|
||||
syslogHostname: z.string().optional(),
|
||||
timestamp: z.instanceof(Date).optional(),
|
||||
msgid: z.string().max(32).optional(), // RFC 5424 limit
|
||||
});
|
||||
|
||||
/**
|
||||
* Inferred type from clientOptionsSchema for consistency.
|
||||
*/
|
||||
export type ValidatedClientOptions = z.infer<typeof clientOptionsSchema>;
|
||||
|
||||
/**
|
||||
* Inferred type from logOptionsSchema for consistency.
|
||||
*/
|
||||
export type ValidatedLogOptions = z.infer<typeof logOptionsSchema>;
|
||||
@@ -0,0 +1,147 @@
|
||||
import type * as dgram from 'dgram';
|
||||
import type * as net from 'net';
|
||||
import type * as tls from 'tls';
|
||||
|
||||
import type { Facility, Severity, Transport } from './constants';
|
||||
|
||||
/**
|
||||
* Callback type for legacy API support.
|
||||
*/
|
||||
export type SyslogCallback = (error?: Error) => void;
|
||||
|
||||
/**
|
||||
* Date formatter function type.
|
||||
* Takes a Date object and returns a formatted string.
|
||||
*/
|
||||
export type DateFormatter = (date: Date) => string;
|
||||
|
||||
/**
|
||||
* Options for creating a syslog client.
|
||||
*/
|
||||
export interface ClientOptions {
|
||||
/**
|
||||
* Hostname to use in syslog messages.
|
||||
* @default os.hostname()
|
||||
*/
|
||||
syslogHostname?: string;
|
||||
|
||||
/**
|
||||
* Port number for TCP/TLS/UDP connections.
|
||||
* @default 514
|
||||
*/
|
||||
port?: number;
|
||||
|
||||
/**
|
||||
* TCP connection timeout in milliseconds.
|
||||
* @default 10000
|
||||
*/
|
||||
tcpTimeout?: number;
|
||||
|
||||
/**
|
||||
* Default facility for log messages.
|
||||
* @default Facility.Local0
|
||||
*/
|
||||
facility?: Facility;
|
||||
|
||||
/**
|
||||
* Default severity for log messages.
|
||||
* @default Severity.Informational
|
||||
*/
|
||||
severity?: Severity;
|
||||
|
||||
/**
|
||||
* Use RFC 3164 format (true) or RFC 5424 format (false).
|
||||
* @default true
|
||||
*/
|
||||
rfc3164?: boolean;
|
||||
|
||||
/**
|
||||
* Application name for RFC 5424 format.
|
||||
* @default process.title
|
||||
*/
|
||||
appName?: string;
|
||||
|
||||
/**
|
||||
* Custom date formatter function.
|
||||
* @default Date.prototype.toISOString
|
||||
*/
|
||||
dateFormatter?: DateFormatter;
|
||||
|
||||
/**
|
||||
* UDP bind address for outgoing datagrams.
|
||||
* If not specified, node will bind to 0.0.0.0.
|
||||
*/
|
||||
udpBindAddress?: string;
|
||||
|
||||
/**
|
||||
* Transport protocol to use.
|
||||
* @default Transport.Udp
|
||||
*/
|
||||
transport?: Transport;
|
||||
|
||||
/**
|
||||
* TLS CA certificate(s). Only used when transport is Transport.Tls.
|
||||
*/
|
||||
tlsCA?: string | string[] | Buffer | Buffer[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for individual log messages.
|
||||
* These override the client defaults for a single message.
|
||||
*/
|
||||
export interface LogOptions {
|
||||
/**
|
||||
* Override facility for this message.
|
||||
*/
|
||||
facility?: Facility;
|
||||
|
||||
/**
|
||||
* Override severity for this message.
|
||||
*/
|
||||
severity?: Severity;
|
||||
|
||||
/**
|
||||
* Override RFC format for this message.
|
||||
*/
|
||||
rfc3164?: boolean;
|
||||
|
||||
/**
|
||||
* Override app name for this message.
|
||||
*/
|
||||
appName?: string;
|
||||
|
||||
/**
|
||||
* Override syslog hostname for this message.
|
||||
*/
|
||||
syslogHostname?: string;
|
||||
|
||||
/**
|
||||
* Custom timestamp for the message.
|
||||
* Useful for back-dating messages based on external timestamps.
|
||||
*/
|
||||
timestamp?: Date;
|
||||
|
||||
/**
|
||||
* Message ID for RFC 5424 format.
|
||||
* @default "-"
|
||||
*/
|
||||
msgid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal type for resolved log options with all defaults applied.
|
||||
*/
|
||||
export interface ResolvedLogOptions {
|
||||
facility: Facility;
|
||||
severity: Severity;
|
||||
rfc3164: boolean;
|
||||
appName: string;
|
||||
syslogHostname: string;
|
||||
timestamp?: Date;
|
||||
msgid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type for all possible transport implementations.
|
||||
*/
|
||||
export type TransportConnection = dgram.Socket | net.Socket | tls.TLSSocket;
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { DateFormatter, ResolvedLogOptions } from './types';
|
||||
|
||||
/**
|
||||
* Default date formatter for RFC 5424 format.
|
||||
* Returns ISO 8601 timestamp.
|
||||
*/
|
||||
export const defaultDateFormatter: DateFormatter = (date) => date.toISOString();
|
||||
|
||||
/**
|
||||
* Format RFC 3164 timestamp.
|
||||
* Example: "Jan 15 08:30:00"
|
||||
*
|
||||
* Note: BSD syslog requires leading 0's in day to be a space.
|
||||
*/
|
||||
export const formatRfc3164Timestamp = (date: Date): string => {
|
||||
const elements = date.toString().split(/\s+/);
|
||||
const month = elements[1];
|
||||
let day = elements[2];
|
||||
const time = elements[4];
|
||||
|
||||
// BSD syslog requires leading 0's to be a space
|
||||
if (day[0] === '0') {
|
||||
day = ' ' + day.substring(1);
|
||||
}
|
||||
|
||||
return `${month} ${day} ${time}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build formatted syslog message according to RFC 3164 or RFC 5424.
|
||||
*
|
||||
* @param message - The message to format
|
||||
* @param options - Resolved log options with all defaults applied
|
||||
* @param dateFormatter - Date formatter function
|
||||
* @returns Buffer containing the formatted syslog message
|
||||
*/
|
||||
export const buildFormattedMessage = (
|
||||
message: string,
|
||||
options: ResolvedLogOptions,
|
||||
dateFormatter: DateFormatter,
|
||||
): Buffer => {
|
||||
const date = options.timestamp ?? new Date();
|
||||
const pri = options.facility * 8 + options.severity;
|
||||
const newline = message.endsWith('\n') ? '' : '\n';
|
||||
|
||||
let formattedMessage: string;
|
||||
|
||||
if (options.rfc3164) {
|
||||
// RFC 3164 format: <PRI>TIMESTAMP HOSTNAME MESSAGE
|
||||
const timestamp = formatRfc3164Timestamp(date);
|
||||
formattedMessage = `<${pri}>${timestamp} ${options.syslogHostname} ${message}${newline}`;
|
||||
} else {
|
||||
// RFC 5424 format: <PRI>VERSION TIMESTAMP HOSTNAME APP-NAME PROCID MSGID SD MESSAGE
|
||||
const timestamp = dateFormatter(date);
|
||||
const msgid = options.msgid ?? '-';
|
||||
formattedMessage = `<${pri}>1 ${timestamp} ${options.syslogHostname} ${options.appName} ${process.pid} ${msgid} - ${message}${newline}`;
|
||||
}
|
||||
|
||||
return Buffer.from(formattedMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an address is IPv6.
|
||||
* Simple check based on presence of colons.
|
||||
*
|
||||
* @param address - IP address to check
|
||||
* @returns true if IPv6, false otherwise
|
||||
*/
|
||||
export const isIPv6 = (address: string): boolean => address.includes(':');
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as os from 'os';
|
||||
|
||||
import { Facility, Severity, SyslogClient, Transport } from '../src';
|
||||
import { TLS_CERTIFICATE } from './setup';
|
||||
|
||||
describe('SyslogClient - Core', () => {
|
||||
describe('Constructor and defaults', () => {
|
||||
it('should set default options correctly', () => {
|
||||
const client = new SyslogClient();
|
||||
expect(client.target).toBe('127.0.0.1');
|
||||
expect(client.port).toBe(514);
|
||||
expect(client.syslogHostname).toBe(os.hostname());
|
||||
expect(client.tcpTimeout).toBe(10000);
|
||||
expect(client.transport).toBe(Transport.Udp);
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept target parameter', () => {
|
||||
const client = new SyslogClient('127.0.0.2');
|
||||
expect(client.target).toBe('127.0.0.2');
|
||||
expect(client.port).toBe(514);
|
||||
expect(client.syslogHostname).toBe(os.hostname());
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept custom hostname', () => {
|
||||
const client = new SyslogClient('127.0.0.2', {
|
||||
syslogHostname: 'test',
|
||||
});
|
||||
expect(client.target).toBe('127.0.0.2');
|
||||
expect(client.syslogHostname).toBe('test');
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept custom port and timeout', () => {
|
||||
const client = new SyslogClient('127.0.0.2', {
|
||||
syslogHostname: 'test',
|
||||
port: 5555,
|
||||
tcpTimeout: 50,
|
||||
});
|
||||
expect(client.port).toBe(5555);
|
||||
expect(client.tcpTimeout).toBe(50);
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept TCP transport option', () => {
|
||||
const client = new SyslogClient('127.0.0.2', {
|
||||
port: 5555,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
expect(client.transport).toBe(Transport.Tcp);
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept TLS transport with certificate', () => {
|
||||
const client = new SyslogClient('127.0.0.2', {
|
||||
port: 6514,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
expect(client.transport).toBe(Transport.Tls);
|
||||
expect(client.tlsCA).toBe(TLS_CERTIFICATE);
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept facility and severity options', () => {
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
facility: Facility.Mail,
|
||||
severity: Severity.Critical,
|
||||
});
|
||||
expect(client.facility).toBe(Facility.Mail);
|
||||
expect(client.severity).toBe(Severity.Critical);
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept RFC format option', () => {
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
rfc3164: false,
|
||||
});
|
||||
expect(client.rfc3164).toBe(false);
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
# Test Fixtures
|
||||
|
||||
This directory contains test certificates for TLS testing.
|
||||
|
||||
## Required Files
|
||||
|
||||
These files can be regenerated using the following commands:
|
||||
|
||||
- `key.pem` - Private key for test TLS server
|
||||
- `certificate.pem` - Valid certificate for test TLS server
|
||||
- `wrong.pem` - Invalid certificate for testing certificate validation
|
||||
|
||||
## Generate Test Certificates
|
||||
|
||||
You can generate self-signed test certificates using OpenSSL:
|
||||
|
||||
```bash
|
||||
# Generate private key
|
||||
openssl genrsa -out key.pem 2048
|
||||
|
||||
# Generate valid certificate
|
||||
openssl req -new -x509 -key key.pem -out certificate.pem -days 10000 \
|
||||
-subj "/C=US/ST=Test/L=Test/O=Test/CN=localhost"
|
||||
|
||||
# Generate wrong certificate (different key)
|
||||
openssl genrsa -out wrong-key.pem 2048
|
||||
openssl req -new -x509 -key wrong-key.pem -out wrong.pem -days 10000 \
|
||||
-subj "/C=US/ST=Wrong/L=Wrong/O=Wrong/CN=wrong"
|
||||
rm wrong-key.pem
|
||||
```
|
||||
|
||||
These certificates are only for testing and should never be used in production.
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDfzCCAmegAwIBAgIUEo4pU1hgsKHlV01QIN3hhDaQ6ggwDQYJKoZIhvcNAQEL
|
||||
BQAwTjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
|
||||
DTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNjAxMDUyMDQ1
|
||||
NDhaGA8yMDUzMDUyMzIwNDU0OFowTjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRl
|
||||
c3QxDTALBgNVBAcMBFRlc3QxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2Fs
|
||||
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMzoP33jcgWYJ/mS
|
||||
A8NQm9gABgeOVHlc4lEqHsMo9gZy3GfmanI/XFpTr3Nxeb2AMqJ1B9EgXOtJLBDP
|
||||
J+Z+k3jN9Sv2ZRSClf6ZZUUPFKcjQ6TNYXG/Jgmjoku7k0I9cJluXE8VwlC9mzty
|
||||
iHbZGo/tRtU4LkbaedmL2eTctcBukX4g99uVMJVKot5l8PWxZ1seVZ6T4IiRcaRo
|
||||
NcQqL67xspm3KmlrVx/WzlM41BfGj2k+L4Bgenm/bIg1RPQ+MoETViVFGeKgKjkp
|
||||
kTOjCsZqeK2STCIj1+XI4tLMCxV0B95fw7bcFfEtbnL9XyEjyn+3A33625EJMoiq
|
||||
OX6bb9MCAwEAAaNTMFEwHQYDVR0OBBYEFEB7f6J6nW90lXS8i4gOR/6rya7qMB8G
|
||||
A1UdIwQYMBaAFEB7f6J6nW90lXS8i4gOR/6rya7qMA8GA1UdEwEB/wQFMAMBAf8w
|
||||
DQYJKoZIhvcNAQELBQADggEBAIXoFrV9saNREtlDkMp5O66IO+UFhr1NcLYmXvhs
|
||||
n3H+OFpg/Uo+/p++vHgf5UKNTdx24pO/88FyPujIcq1mfemrAIgcd8t++aclzSlf
|
||||
G49bZvhYyxsE+qoyl7XWtmYLPydCpYKkL7krLwUVzSeC/f6f/GWEn0vvwWPht5ky
|
||||
dR/Uze7pg/Ux12gztSLPV77n9gBua6lj5HKhXYGoIFuZSw9n1tWqndT28OQ3VwI7
|
||||
ZSpFFJmDyevspC2F18NaTE5VmXv9gckeUmqx8nr0zOmWWREVnFt+d4NNvrNLEChj
|
||||
IJFfkzynUHYTe+1oQkv8Evr0GHU5ocHsqOQ4NqW5gx0XXRA=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDM6D9943IFmCf5
|
||||
kgPDUJvYAAYHjlR5XOJRKh7DKPYGctxn5mpyP1xaU69zcXm9gDKidQfRIFzrSSwQ
|
||||
zyfmfpN4zfUr9mUUgpX+mWVFDxSnI0OkzWFxvyYJo6JLu5NCPXCZblxPFcJQvZs7
|
||||
coh22RqP7UbVOC5G2nnZi9nk3LXAbpF+IPfblTCVSqLeZfD1sWdbHlWek+CIkXGk
|
||||
aDXEKi+u8bKZtyppa1cf1s5TONQXxo9pPi+AYHp5v2yINUT0PjKBE1YlRRnioCo5
|
||||
KZEzowrGanitkkwiI9flyOLSzAsVdAfeX8O23BXxLW5y/V8hI8p/twN9+tuRCTKI
|
||||
qjl+m2/TAgMBAAECggEARqkwe/EbOOKnpmC7+3aox2v6qFBkwRVO1j54LUTK0p55
|
||||
czr6Kju/VUbWkjDnknnK/ErvTyah5GFvWLyXPd0YbehYS7jEUrZVk17ClXYF4T0/
|
||||
7m0E1XzdWSWcqhEdTxJw6fgszPjr8XvxNCbi+FkV19wzOQQOsVBWBLc9hLa31ous
|
||||
CCMmpmJD+KaaN1D7VKDyLOtMQDPM7xft9VcWTU0qATN86HzvTxRF3+wWi1Z8ChV+
|
||||
miMfu4yb4Ip/tFSZfCJQqWupxdXYxw+p6HbcYRxSFa8ux4oZoQwvSVIi3pGKFXij
|
||||
/0oBhx2N1Y0T5LnuUZvne6CjwSyRSCmN68FnUKiogQKBgQD5UuHXcMZiDyZSskpj
|
||||
6rj//Hc3I9eRvB2CIvxVbDecRoCat3r9SZmZJ+DFxyH9Voy0ClrQnjSjPsXR5T89
|
||||
Ya+dFELl0zg7scHMw9VVSBsIjPrBhCnqPII1tfZj5ZzKI0oM1P8DwsoBOMotZIjl
|
||||
eHsUsd3Qw5sogbrwJ5d0cuDcaQKBgQDSZOPLwuEtAc7qyu1lSbifCMYwM4ISac3F
|
||||
2f116BV43/9L8eQv41XmVl1ELmwebahQdIbTtP5bNGDRHLRyIfbsSWxzBamQkKX9
|
||||
INzsiaeCENMTEHo4oSXX89DoZkr50iT62y9DKF5TkzNkA+CMvUuKfkh8YZAnLizA
|
||||
dhrM0NOS2wKBgQD3nJ/ctgzgEEmwMY994fxvhw3i/j62ZswrlZFjSQGFu+M3ROfp
|
||||
q3HXUGqEIbuY/Z7po6sDq0t9oPcX+QQwctbVOu0hkP81Edn2AbvaYa0vdcwH6rkx
|
||||
/3wV2axlFAH+IsQFMHhABPFX+02XfVQUCe6649b24X0z4nuEzN028mxtmQKBgC7q
|
||||
XHmVbUzx+EgXFDTf6ZFdDYS2F60vdrlF0OU36YHYwT8YI4AiO4dvfsCzcVyfq0a1
|
||||
lVMkKwv/dA9tTTeeJ0etX8eOXa9k8f6uE1WTpIy2X7sCk56JIL01G0KXfOSUXbaq
|
||||
pbFeERql2nHVA3+evneVjWhfARwbidMpAvTlJCi9AoGAQxFKi/dKzjZQFkikv9xU
|
||||
QpvtrfjZvYHU56HMDkKA4yIjBpONPWc6TAir5eOroKIKxAF0K+5s/bcQqZagShfZ
|
||||
WhZSdUid4J+gPCYL2v5F8symu9dxP54xRw1886uJUuqyb7tHhdFCOHRclD5wlLg3
|
||||
OrfEUwhYRn7d21UZoGDAvYU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Generate valid certificate
|
||||
openssl req -new -x509 -key key.pem -out certificate.pem -days 10000 \
|
||||
-subj "/C=US/ST=Test/L=Test/O=Test/CN=localhost"
|
||||
|
||||
# Generate wrong certificate (different key)
|
||||
openssl genrsa -out wrong-key.pem 2048
|
||||
openssl req -new -x509 -key wrong-key.pem -out wrong.pem -days 10000 \
|
||||
-subj "/C=US/ST=Wrong/L=Wrong/O=Wrong/CN=wrong"
|
||||
rm wrong-key.pem
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDfTCCAmWgAwIBAgIUfC52wMYMzcoL9c/GY3Ks0beSH2kwDQYJKoZIhvcNAQEL
|
||||
BQAwTTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVdyb25nMQ4wDAYDVQQHDAVXcm9u
|
||||
ZzEOMAwGA1UECgwFV3JvbmcxDjAMBgNVBAMMBXdyb25nMCAXDTI2MDEwNTIwNDU0
|
||||
OFoYDzIwNTMwNTIzMjA0NTQ4WjBNMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFV3Jv
|
||||
bmcxDjAMBgNVBAcMBVdyb25nMQ4wDAYDVQQKDAVXcm9uZzEOMAwGA1UEAwwFd3Jv
|
||||
bmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCj1725lupt3pTvMf2O
|
||||
o931s2l4ObA5qTC6ihlWJX+A7R4RkHrQTlwahEwIgMNSzNURtXOwu3ePyqZE3Luk
|
||||
pVWGMFdgedNuU+4zNUXXamIyIJfsMuEmw6xrWLpdWqawo0G50Y2RxB4MN6/127PL
|
||||
EqogXC+0PHdgBUWtso/sJcGRrdQvGpmH9ZXavVNhER4G3q6f/Tcp1eu0xry1UwGV
|
||||
WN/DghkE5R/9mLm830Lr7gLpEQ6JKUUTnPKaZQW9m9G8gjr6oEXR9QaTcTariHgx
|
||||
HT0wkGRKO54CC3RdBxqwucdkvgUI7OkaGl8tS46QMI4DwQHcUFLkqx29wXHY/Zhu
|
||||
mHxDAgMBAAGjUzBRMB0GA1UdDgQWBBT8E+hqfJTcai2AlqT27R4KL/nGNTAfBgNV
|
||||
HSMEGDAWgBT8E+hqfJTcai2AlqT27R4KL/nGNTAPBgNVHRMBAf8EBTADAQH/MA0G
|
||||
CSqGSIb3DQEBCwUAA4IBAQCf38uZYe6+cW7/juw2wCoLObSBoomRuJicwpRbDJpo
|
||||
kzKzlBm4KbAwrz0+2kugZCrqrY1pLZPozQiqqf7UOImNcBi/wCbNk6iksdD9WRpv
|
||||
tooKb2UHY9Z+1VJ7FqBhEekUJnxAoOlmOnSwPW3qDQNaP+niWZEZjgK+m+3/ZI5d
|
||||
oW8lU1UGYJs/pZrZDwPUGHRnPj7Sa6q0S5OCVFx4BN+YMKN9ynvVG0z0pkO3QpA3
|
||||
wOYu/WxINCuFb6Iu469Ov/FoSfnqZRcqZ+PlMDWHr0NCj0DNBP/eGq8VWy6VRabQ
|
||||
SyRRjByPFe8fK1PAbrwIh8w6QIWz28OQZjAz0TsHnK7D
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,285 @@
|
||||
import { SyslogClient, Transport } from '../src';
|
||||
import {
|
||||
awaitUdpMsg,
|
||||
constructRfc5424Regex,
|
||||
constructSyslogRegex,
|
||||
startTestServers,
|
||||
stopTestServers,
|
||||
SYSLOG_UDP_PORT,
|
||||
} from './setup';
|
||||
|
||||
beforeAll(async () => {
|
||||
await startTestServers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
stopTestServers();
|
||||
});
|
||||
|
||||
describe('SyslogClient - Message Formats', () => {
|
||||
describe('RFC 3164 (BSD syslog)', () => {
|
||||
it('should send RFC 3164 format by default', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('RFC 3164 test');
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'RFC 3164 test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should send RFC 3164 format when explicitly enabled', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
rfc3164: true,
|
||||
});
|
||||
|
||||
await client.log('RFC 3164 explicit');
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'RFC 3164 explicit'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should send back-dated RFC 3164 messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const backdate = new Date(2017, 2, 1);
|
||||
await client.log('Back-dated test', { timestamp: backdate });
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Back-dated test', backdate));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should handle messages with custom options', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('Custom options', {
|
||||
rfc3164: true,
|
||||
msgid: '12345', // msgid is ignored in RFC 3164
|
||||
});
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Custom options'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RFC 5424 (modern syslog)', () => {
|
||||
it('should send RFC 5424 format when enabled', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
rfc3164: false,
|
||||
});
|
||||
|
||||
await client.log('RFC 5424 test');
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'RFC 5424 test', '-'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should use default msgid when not provided', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('Default msgid', { rfc3164: false });
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'Default msgid', '-'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should use custom msgid when provided', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('Custom msgid', {
|
||||
rfc3164: false,
|
||||
msgid: '98765',
|
||||
});
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'Custom msgid', '98765'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should accept msgid up to 32 characters (RFC 5424 limit)', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const maxLengthMsgid = '550e8400e29b41d4a716446655440000'; // 32 chars (UUID without hyphens)
|
||||
await client.log('Max length msgid', {
|
||||
rfc3164: false,
|
||||
msgid: maxLengthMsgid,
|
||||
});
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructRfc5424Regex(134, hostname, 'Max length msgid', maxLengthMsgid));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should reject msgid longer than 32 characters (RFC 5424 limit)', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const tooLongMsgid = '550e8400-e29b-41d4-a716-446655440000'; // 36 chars (UUID with hyphens)
|
||||
await expect(
|
||||
client.log('Too long msgid', {
|
||||
rfc3164: false,
|
||||
msgid: tooLongMsgid,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should send back-dated RFC 5424 messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const backdate = new Date(2017, 2, 1);
|
||||
await client.log('Back-dated RFC 5424', {
|
||||
rfc3164: false,
|
||||
msgid: '98765',
|
||||
timestamp: backdate,
|
||||
});
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(
|
||||
constructRfc5424Regex(134, hostname, 'Back-dated RFC 5424', '98765', backdate),
|
||||
);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should include process ID in RFC 5424 messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
rfc3164: false,
|
||||
});
|
||||
|
||||
await client.log('Process ID test');
|
||||
const msg = await awaitUdpMsg();
|
||||
|
||||
// Check that message contains a process ID (numeric)
|
||||
expect(msg).toMatch(/\d+ - - Process ID test/);
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Format switching', () => {
|
||||
it('should allow switching format per message', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
rfc3164: true, // Default to RFC 3164
|
||||
});
|
||||
|
||||
// Send RFC 3164 message (default)
|
||||
await client.log('Message 1');
|
||||
const msg1 = await awaitUdpMsg();
|
||||
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'Message 1'));
|
||||
|
||||
// Send RFC 5424 message (override)
|
||||
await client.log('Message 2', { rfc3164: false });
|
||||
const msg2 = await awaitUdpMsg();
|
||||
expect(msg2).toMatch(constructRfc5424Regex(134, hostname, 'Message 2', '-'));
|
||||
|
||||
// Send RFC 3164 message again (back to default)
|
||||
await client.log('Message 3');
|
||||
const msg3 = await awaitUdpMsg();
|
||||
expect(msg3).toMatch(constructSyslogRegex(134, hostname, 'Message 3'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message content', () => {
|
||||
it('should preserve newlines in message', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const messageWithNewline = 'Message with\nnewline';
|
||||
await client.log(messageWithNewline);
|
||||
const msg = await awaitUdpMsg();
|
||||
|
||||
expect(msg).toContain(messageWithNewline);
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should handle empty messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('');
|
||||
const msg = await awaitUdpMsg();
|
||||
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, ''));
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import * as dgram from 'dgram';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as readline from 'readline';
|
||||
import * as tls from 'tls';
|
||||
|
||||
// Test ports - dynamically assigned by getAvailablePort
|
||||
export let SYSLOG_UDP_PORT = 0;
|
||||
export let SYSLOG_TCP_PORT = 0;
|
||||
export let SYSLOG_TLS_PORT = 0;
|
||||
|
||||
/**
|
||||
* Get an available port by creating and immediately closing a server.
|
||||
*/
|
||||
const getAvailablePort = async (): Promise<number> =>
|
||||
await new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.unref();
|
||||
server.on('error', reject);
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
server.close();
|
||||
return reject(new Error('Unable to get port'));
|
||||
}
|
||||
const { port } = address;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
|
||||
// Test certificates
|
||||
export const TLS_PRIVATE_KEY = fs.readFileSync(path.join(__dirname, 'fixtures', 'key.pem'), 'utf8');
|
||||
export const TLS_CERTIFICATE = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'certificate.pem'),
|
||||
'utf8',
|
||||
);
|
||||
export const WRONG_CERTIFICATE = fs.readFileSync(
|
||||
path.join(__dirname, 'fixtures', 'wrong.pem'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// Message queues
|
||||
const queuedUdpMessages: string[] = [];
|
||||
const pendingUdpResolvers: Array<(msg: string) => void> = [];
|
||||
const queuedTcpMessages: string[] = [];
|
||||
const pendingTcpResolvers: Array<(msg: string) => void> = [];
|
||||
const queuedTlsMessages: string[] = [];
|
||||
const pendingTlsResolvers: Array<(msg: string) => void> = [];
|
||||
|
||||
// Test servers
|
||||
let udpServer: dgram.Socket | null = null;
|
||||
let tcpServer: net.Server | null = null;
|
||||
let tlsServer: tls.Server | null = null;
|
||||
|
||||
/**
|
||||
* Helper to await syslog messages from UDP server.
|
||||
*/
|
||||
export const awaitUdpMsg = async (): Promise<string> =>
|
||||
await new Promise((resolve) => {
|
||||
const queued = queuedUdpMessages.shift();
|
||||
if (queued) return resolve(queued);
|
||||
pendingUdpResolvers.push(resolve);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to await syslog messages from TCP server.
|
||||
*/
|
||||
export const awaitTcpMsg = async (): Promise<string> =>
|
||||
await new Promise((resolve) => {
|
||||
const queued = queuedTcpMessages.shift();
|
||||
if (queued) return resolve(queued);
|
||||
pendingTcpResolvers.push(resolve);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to await syslog messages from TLS server.
|
||||
*/
|
||||
export const awaitTlsMsg = async (): Promise<string> =>
|
||||
await new Promise((resolve) => {
|
||||
const queued = queuedTlsMessages.shift();
|
||||
if (queued) return resolve(queued);
|
||||
pendingTlsResolvers.push(resolve);
|
||||
});
|
||||
|
||||
/**
|
||||
* Escape special regex characters.
|
||||
*/
|
||||
const escapeRegExp = (text: string): string => text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
||||
|
||||
/**
|
||||
* Construct regex to match RFC 3164 syslog message.
|
||||
*/
|
||||
export const constructSyslogRegex = (
|
||||
pri: number,
|
||||
hostname: string,
|
||||
msg: string,
|
||||
timestamp?: Date,
|
||||
): RegExp => {
|
||||
let patDate: string;
|
||||
if (!timestamp) {
|
||||
// RFC-3164 date format: "MMM dd HH:mm:ss"
|
||||
// Month is 3 letters, day is space-padded, time is in 24-hour format
|
||||
patDate = '[A-Z][a-z]{2}\\s{1,2}\\d{1,2}\\s\\d{2}:\\d{2}:\\d{2}';
|
||||
} else {
|
||||
const elements = timestamp.toString().split(/\s+/);
|
||||
const month = elements[1];
|
||||
let day = elements[2];
|
||||
const time = elements[4];
|
||||
|
||||
// Ensure day is space-padded for single digits as per RFC-3164
|
||||
if (day.length === 2 && day[0] === '0') {
|
||||
day = ' ' + day.substring(1);
|
||||
}
|
||||
|
||||
patDate = escapeRegExp(`${month} ${day} ${time}`);
|
||||
}
|
||||
|
||||
// RFC-3164 format: <PRI>TIMESTAMP HOSTNAME MSG
|
||||
// - PRI is enclosed in angle brackets
|
||||
// - Single space between timestamp and hostname
|
||||
// - Single space between hostname and message
|
||||
// - Optional newline at the end
|
||||
return new RegExp(
|
||||
`^<${escapeRegExp(String(pri))}>${patDate}\\s${escapeRegExp(hostname)}\\s${escapeRegExp(msg)}\\n?$`,
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Construct regex to match RFC 5424 syslog message.
|
||||
*/
|
||||
export const constructRfc5424Regex = (
|
||||
pri: number,
|
||||
hostname: string,
|
||||
msg: string,
|
||||
msgid: string | number,
|
||||
timestamp?: Date,
|
||||
): RegExp => {
|
||||
const patDate = !timestamp
|
||||
? '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z'
|
||||
: escapeRegExp(timestamp.toISOString());
|
||||
|
||||
return new RegExp(
|
||||
`^<${escapeRegExp(String(pri))}>\\d+ ${patDate} ${escapeRegExp(hostname)} \\S{1,48} \\d+ ${escapeRegExp(String(msgid))} - ${escapeRegExp(msg)}\\n?$`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start test syslog servers (UDP, TCP, TLS).
|
||||
*/
|
||||
export const startTestServers = async (): Promise<void> => {
|
||||
// Get available ports
|
||||
SYSLOG_UDP_PORT = await getAvailablePort();
|
||||
SYSLOG_TCP_PORT = await getAvailablePort();
|
||||
SYSLOG_TLS_PORT = await getAvailablePort();
|
||||
|
||||
// Setup UDP server
|
||||
await new Promise<void>((resolve) => {
|
||||
udpServer = dgram.createSocket('udp4');
|
||||
udpServer.on('message', (msg) => {
|
||||
const resolver = pendingUdpResolvers.shift();
|
||||
if (resolver) return resolver(msg.toString());
|
||||
queuedUdpMessages.push(msg.toString());
|
||||
});
|
||||
udpServer.on('listening', () => {
|
||||
console.log(`Started UDP syslog server on port ${SYSLOG_UDP_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
udpServer.bind(SYSLOG_UDP_PORT);
|
||||
});
|
||||
|
||||
// Setup TCP server
|
||||
await new Promise<void>((resolve) => {
|
||||
tcpServer = net.createServer((socket) => {
|
||||
const lines = readline.createInterface({ input: socket, output: socket });
|
||||
lines.on('line', (line) => {
|
||||
const resolver = pendingTcpResolvers.shift();
|
||||
if (resolver) return resolver(line);
|
||||
queuedTcpMessages.push(line);
|
||||
});
|
||||
});
|
||||
tcpServer.listen(SYSLOG_TCP_PORT, () => {
|
||||
console.log(`Started TCP syslog server on port ${SYSLOG_TCP_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Setup TLS server
|
||||
await new Promise<void>((resolve) => {
|
||||
tlsServer = tls.createServer(
|
||||
{
|
||||
key: TLS_PRIVATE_KEY,
|
||||
cert: TLS_CERTIFICATE,
|
||||
secureProtocol: 'TLSv1_2_method',
|
||||
},
|
||||
(socket) => {
|
||||
const lines = readline.createInterface({ input: socket, output: socket });
|
||||
lines.on('line', (line) => {
|
||||
const resolver = pendingTlsResolvers.shift();
|
||||
if (resolver) return resolver(line);
|
||||
queuedTlsMessages.push(line);
|
||||
});
|
||||
},
|
||||
);
|
||||
tlsServer.listen(SYSLOG_TLS_PORT, () => {
|
||||
console.log(`Started TLS syslog server on port ${SYSLOG_TLS_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop test syslog servers.
|
||||
*/
|
||||
export const stopTestServers = (): void => {
|
||||
udpServer?.close();
|
||||
tcpServer?.close();
|
||||
tlsServer?.close();
|
||||
udpServer = null;
|
||||
tcpServer = null;
|
||||
tlsServer = null;
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
import { Facility, Severity, SyslogClient, Transport } from '../src';
|
||||
import {
|
||||
awaitTcpMsg,
|
||||
constructSyslogRegex,
|
||||
startTestServers,
|
||||
stopTestServers,
|
||||
SYSLOG_TCP_PORT,
|
||||
} from './setup';
|
||||
|
||||
beforeAll(async () => {
|
||||
await startTestServers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
stopTestServers();
|
||||
});
|
||||
|
||||
describe('SyslogClient - TCP Transport', () => {
|
||||
describe('Basic functionality', () => {
|
||||
it('should connect and send log messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
await client.log('This is a test');
|
||||
const msg1 = await awaitTcpMsg();
|
||||
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'This is a test'));
|
||||
|
||||
await client.log('This is a second test');
|
||||
const msg2 = await awaitTcpMsg();
|
||||
expect(msg2).toMatch(constructSyslogRegex(134, hostname, 'This is a second test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should work with promise API', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
await client.log('Promise test');
|
||||
const msg = await awaitTcpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Promise test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should work with callback API', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
const callbackPromise = new Promise<void>((resolve) => {
|
||||
void client.log('Callback test', (error) => {
|
||||
expect(error).toBeUndefined();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const msg = await awaitTcpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Callback test'));
|
||||
await callbackPromise;
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transport reuse', () => {
|
||||
it('should reuse TCP transport for multiple messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
await client.log('Transport reuse test');
|
||||
await awaitTcpMsg();
|
||||
const transport1 = client['transport_'];
|
||||
expect(transport1).toBeDefined();
|
||||
|
||||
await client.log('Transport reuse test 2');
|
||||
await awaitTcpMsg();
|
||||
const transport2 = client['transport_'];
|
||||
expect(transport2).toBe(transport1);
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Close and reconnect', () => {
|
||||
it('should emit close event', async () => {
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
client.once('close', resolve);
|
||||
});
|
||||
|
||||
await client.log('Test');
|
||||
await awaitTcpMsg();
|
||||
client.close();
|
||||
await closePromise;
|
||||
|
||||
expect(client['transport_']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reconnect after close', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
await client.log('Before close');
|
||||
await awaitTcpMsg();
|
||||
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
client.once('close', resolve);
|
||||
});
|
||||
client.close();
|
||||
await closePromise;
|
||||
|
||||
await client.log('After reconnect');
|
||||
const msg = await awaitTcpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'After reconnect'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Log options', () => {
|
||||
it('should accept options with callback', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
const callbackPromise = new Promise<void>((resolve) => {
|
||||
void client.log(
|
||||
'With options',
|
||||
{
|
||||
facility: Facility.System,
|
||||
severity: Severity.Notice,
|
||||
},
|
||||
(error) => {
|
||||
expect(error).toBeUndefined();
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const msg = await awaitTcpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(29, hostname, 'With options'));
|
||||
await callbackPromise;
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should calculate correct PRI value', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
await client.log('Test', {
|
||||
facility: Facility.Local0,
|
||||
severity: Severity.Emergency,
|
||||
});
|
||||
|
||||
const msg = await awaitTcpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(128, hostname, 'Test'));
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle connection timeout', async () => {
|
||||
const client = new SyslogClient('203.0.113.1', {
|
||||
port: SYSLOG_TCP_PORT,
|
||||
tcpTimeout: 500,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
|
||||
const errorPromise = new Promise<Error>((resolve) => {
|
||||
client.on('error', resolve);
|
||||
});
|
||||
|
||||
await expect(client.log("Shouldn't work")).rejects.toThrow();
|
||||
const error = await errorPromise;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('should handle invalid port', () => {
|
||||
expect(() => {
|
||||
new SyslogClient('127.0.0.1', {
|
||||
port: 502342323,
|
||||
tcpTimeout: 2000,
|
||||
transport: Transport.Tcp,
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { ConnectionError, SyslogClient, Transport } from '../src';
|
||||
import {
|
||||
awaitTlsMsg,
|
||||
constructSyslogRegex,
|
||||
startTestServers,
|
||||
stopTestServers,
|
||||
SYSLOG_TLS_PORT,
|
||||
TLS_CERTIFICATE,
|
||||
WRONG_CERTIFICATE,
|
||||
} from './setup';
|
||||
|
||||
beforeAll(async () => {
|
||||
await startTestServers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
stopTestServers();
|
||||
});
|
||||
|
||||
describe('SyslogClient - TLS Transport', () => {
|
||||
describe('Basic functionality', () => {
|
||||
it('should connect and send log messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
await client.log('This is a test');
|
||||
const msg1 = await awaitTlsMsg();
|
||||
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'This is a test'));
|
||||
|
||||
await client.log('This is a second test');
|
||||
const msg2 = await awaitTlsMsg();
|
||||
expect(msg2).toMatch(constructSyslogRegex(134, hostname, 'This is a second test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should work with promise API', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
await client.log('Promise test');
|
||||
const msg = await awaitTlsMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Promise test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should work with callback API', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
const callbackPromise = new Promise<void>((resolve) => {
|
||||
void client.log('Callback test', (error) => {
|
||||
expect(error).toBeUndefined();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const msg = await awaitTlsMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Callback test'));
|
||||
await callbackPromise;
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transport reuse', () => {
|
||||
it('should reuse TLS transport for multiple messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
await client.log('Transport reuse test');
|
||||
await awaitTlsMsg();
|
||||
const transport1 = client['transport_'];
|
||||
expect(transport1).toBeDefined();
|
||||
|
||||
await client.log('Transport reuse test 2');
|
||||
await awaitTlsMsg();
|
||||
const transport2 = client['transport_'];
|
||||
expect(transport2).toBe(transport1);
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Close and reconnect', () => {
|
||||
it('should emit close event', async () => {
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
client.once('close', resolve);
|
||||
});
|
||||
|
||||
await client.log('Test');
|
||||
await awaitTlsMsg();
|
||||
client.close();
|
||||
await closePromise;
|
||||
|
||||
expect(client['transport_']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reconnect after close', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
await client.log('Before close');
|
||||
await awaitTlsMsg();
|
||||
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
client.once('close', resolve);
|
||||
});
|
||||
client.close();
|
||||
await closePromise;
|
||||
|
||||
await client.log('After reconnect');
|
||||
const msg = await awaitTlsMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'After reconnect'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate validation', () => {
|
||||
it('should reject invalid certificate', async () => {
|
||||
const client = new SyslogClient('localhost', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: WRONG_CERTIFICATE,
|
||||
});
|
||||
|
||||
const errorPromise = new Promise<Error>((resolve) => {
|
||||
client.on('error', resolve);
|
||||
});
|
||||
|
||||
await expect(client.log('Should error')).rejects.toBeInstanceOf(ConnectionError);
|
||||
|
||||
const error: NodeJS.ErrnoException = await errorPromise;
|
||||
expect(error.code).toBe('DEPTH_ZERO_SELF_SIGNED_CERT');
|
||||
});
|
||||
|
||||
it('should handle connection timeout', async () => {
|
||||
const client = new SyslogClient('203.0.113.1', {
|
||||
port: SYSLOG_TLS_PORT,
|
||||
tcpTimeout: 500,
|
||||
transport: Transport.Tls,
|
||||
tlsCA: TLS_CERTIFICATE,
|
||||
});
|
||||
|
||||
const errorPromise = new Promise<Error>((resolve) => {
|
||||
client.on('error', resolve);
|
||||
});
|
||||
|
||||
await expect(client.log("Shouldn't work")).rejects.toThrow();
|
||||
const error = await errorPromise;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import { SyslogClient, Transport } from '../src';
|
||||
import {
|
||||
awaitUdpMsg,
|
||||
constructSyslogRegex,
|
||||
startTestServers,
|
||||
stopTestServers,
|
||||
SYSLOG_UDP_PORT,
|
||||
} from './setup';
|
||||
|
||||
beforeAll(async () => {
|
||||
await startTestServers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
stopTestServers();
|
||||
});
|
||||
|
||||
describe('SyslogClient - UDP Transport', () => {
|
||||
describe('Basic functionality', () => {
|
||||
it('should connect and send log messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('This is a test');
|
||||
const msg1 = await awaitUdpMsg();
|
||||
expect(msg1).toMatch(constructSyslogRegex(134, hostname, 'This is a test'));
|
||||
|
||||
await client.log('This is a second test');
|
||||
const msg2 = await awaitUdpMsg();
|
||||
expect(msg2).toMatch(constructSyslogRegex(134, hostname, 'This is a second test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should work with promise API', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('Promise test');
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Promise test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should work with callback API', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const callbackPromise = new Promise<void>((resolve) => {
|
||||
void client.log('Callback test', (error) => {
|
||||
expect(error).toBeUndefined();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Callback test'));
|
||||
await callbackPromise;
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transport reuse', () => {
|
||||
it('should reuse UDP transport for multiple messages', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('Transport reuse test');
|
||||
await awaitUdpMsg();
|
||||
const transport1 = client['transport_'];
|
||||
expect(transport1).toBeDefined();
|
||||
|
||||
await client.log('Transport reuse test 2');
|
||||
await awaitUdpMsg();
|
||||
const transport2 = client['transport_'];
|
||||
expect(transport2).toBe(transport1);
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UDP bind address', () => {
|
||||
it('should bind to specific network address', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
udpBindAddress: '127.0.0.1',
|
||||
});
|
||||
|
||||
await client.log('Bind test');
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'Bind test'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('should handle invalid bind address', () => {
|
||||
expect(() => {
|
||||
new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
transport: Transport.Udp,
|
||||
udpBindAddress: '500.500.500.500',
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Close and reconnect', () => {
|
||||
it('should emit close event', async () => {
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
client.once('close', resolve);
|
||||
});
|
||||
|
||||
await client.log('Test');
|
||||
await awaitUdpMsg();
|
||||
client.close();
|
||||
await closePromise;
|
||||
|
||||
expect(client['transport_']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reconnect after close', async () => {
|
||||
const hostname = 'testhostname';
|
||||
const client = new SyslogClient('127.0.0.1', {
|
||||
port: SYSLOG_UDP_PORT,
|
||||
syslogHostname: hostname,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
|
||||
await client.log('Before close');
|
||||
await awaitUdpMsg();
|
||||
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
client.once('close', resolve);
|
||||
});
|
||||
client.close();
|
||||
await closePromise;
|
||||
|
||||
await client.log('After reconnect');
|
||||
const msg = await awaitUdpMsg();
|
||||
expect(msg).toMatch(constructSyslogRegex(134, hostname, 'After reconnect'));
|
||||
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle invalid port', () => {
|
||||
expect(() => {
|
||||
new SyslogClient('127.0.0.1', {
|
||||
port: 12378726362,
|
||||
transport: Transport.Udp,
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["test/**", "src/**/__tests__/**"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"types": ["node", "jest"],
|
||||
"baseUrl": "src",
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"],
|
||||
"references": [{ "path": "../di/tsconfig.build.json" }]
|
||||
}
|
||||
Reference in New Issue
Block a user