@abbacchio/transport 0.1.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.
Files changed (42) hide show
  1. package/dist/client.d.ts +66 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +123 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/encrypt.d.ts +53 -0
  6. package/dist/encrypt.d.ts.map +1 -0
  7. package/dist/encrypt.js +97 -0
  8. package/dist/encrypt.js.map +1 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +10 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/transports/bunyan.d.ts +58 -0
  14. package/dist/transports/bunyan.d.ts.map +1 -0
  15. package/dist/transports/bunyan.js +84 -0
  16. package/dist/transports/bunyan.js.map +1 -0
  17. package/dist/transports/console.d.ts +21 -0
  18. package/dist/transports/console.d.ts.map +1 -0
  19. package/dist/transports/console.js +119 -0
  20. package/dist/transports/console.js.map +1 -0
  21. package/dist/transports/index.d.ts +9 -0
  22. package/dist/transports/index.d.ts.map +1 -0
  23. package/dist/transports/index.js +9 -0
  24. package/dist/transports/index.js.map +1 -0
  25. package/dist/transports/pino.d.ts +31 -0
  26. package/dist/transports/pino.d.ts.map +1 -0
  27. package/dist/transports/pino.js +42 -0
  28. package/dist/transports/pino.js.map +1 -0
  29. package/dist/transports/winston.d.ts +55 -0
  30. package/dist/transports/winston.d.ts.map +1 -0
  31. package/dist/transports/winston.js +85 -0
  32. package/dist/transports/winston.js.map +1 -0
  33. package/package.json +56 -0
  34. package/src/client.ts +148 -0
  35. package/src/encrypt.ts +112 -0
  36. package/src/index.ts +19 -0
  37. package/src/transports/bunyan.ts +99 -0
  38. package/src/transports/console.ts +147 -0
  39. package/src/transports/index.ts +15 -0
  40. package/src/transports/pino.ts +49 -0
  41. package/src/transports/winston.ts +100 -0
  42. package/tsconfig.json +19 -0
package/src/encrypt.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
2
+
3
+ /**
4
+ * Generate a cryptographically secure random key for encryption.
5
+ * Use this on the server/producer side to create a key for each channel.
6
+ *
7
+ * @param length - Length of the key in bytes (default: 32 for 256-bit key)
8
+ * @returns A hex-encoded random key
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { generateKey } from "@abbacchio/client";
13
+ *
14
+ * // Generate a key for a channel
15
+ * const key = generateKey();
16
+ *
17
+ * // Use in pino transport
18
+ * const logger = pino({
19
+ * transport: {
20
+ * target: "@abbacchio/client/transports/pino",
21
+ * options: {
22
+ * url: "http://localhost:4000/api/logs",
23
+ * channel: "my-app",
24
+ * secretKey: key,
25
+ * },
26
+ * },
27
+ * });
28
+ *
29
+ * // Share the dashboard URL with the key
30
+ * console.log(`Dashboard: http://localhost:4000?channel=my-app&key=${key}`);
31
+ * ```
32
+ */
33
+ export function generateKey(length: number = 32): string {
34
+ return randomBytes(length).toString("hex");
35
+ }
36
+
37
+ const ALGORITHM = "aes-256-gcm";
38
+ const IV_LENGTH = 16;
39
+ const AUTH_TAG_LENGTH = 16;
40
+ const SALT_LENGTH = 32;
41
+ const PBKDF2_ITERATIONS = 100000;
42
+
43
+ /**
44
+ * Derive a key from a password using PBKDF2 (browser-compatible)
45
+ */
46
+ function deriveKey(password: string, salt: Buffer): Buffer {
47
+ return pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 32, "sha256");
48
+ }
49
+
50
+ /**
51
+ * Encrypt data with AES-256-GCM
52
+ * Returns base64 encoded string: salt:iv:authTag:ciphertext
53
+ */
54
+ export function encrypt(data: string, secretKey: string): string {
55
+ const salt = randomBytes(SALT_LENGTH);
56
+ const key = deriveKey(secretKey, salt);
57
+ const iv = randomBytes(IV_LENGTH);
58
+
59
+ const cipher = createCipheriv(ALGORITHM, key, iv);
60
+ const encrypted = Buffer.concat([
61
+ cipher.update(data, "utf8"),
62
+ cipher.final(),
63
+ ]);
64
+ const authTag = cipher.getAuthTag();
65
+
66
+ // Combine: salt + iv + authTag + ciphertext
67
+ const combined = Buffer.concat([salt, iv, authTag, encrypted]);
68
+ return combined.toString("base64");
69
+ }
70
+
71
+ /**
72
+ * Decrypt data encrypted with encrypt()
73
+ */
74
+ export function decrypt(encryptedData: string, secretKey: string): string {
75
+ const combined = Buffer.from(encryptedData, "base64");
76
+
77
+ // Extract components
78
+ const salt = combined.subarray(0, SALT_LENGTH);
79
+ const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
80
+ const authTag = combined.subarray(
81
+ SALT_LENGTH + IV_LENGTH,
82
+ SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
83
+ );
84
+ const ciphertext = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
85
+
86
+ const key = deriveKey(secretKey, salt);
87
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
88
+ decipher.setAuthTag(authTag);
89
+
90
+ const decrypted = Buffer.concat([
91
+ decipher.update(ciphertext),
92
+ decipher.final(),
93
+ ]);
94
+
95
+ return decrypted.toString("utf8");
96
+ }
97
+
98
+ /**
99
+ * Encrypt a log object
100
+ */
101
+ export function encryptLog(log: unknown, secretKey: string): { encrypted: string } {
102
+ const jsonStr = JSON.stringify(log);
103
+ return { encrypted: encrypt(jsonStr, secretKey) };
104
+ }
105
+
106
+ /**
107
+ * Decrypt an encrypted log
108
+ */
109
+ export function decryptLog<T = unknown>(encryptedLog: { encrypted: string }, secretKey: string): T {
110
+ const jsonStr = decrypt(encryptedLog.encrypted, secretKey);
111
+ return JSON.parse(jsonStr);
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Core client
2
+ export { AbbacchioClient, createClient } from "./client.js";
3
+ export type { AbbacchioClientOptions } from "./client.js";
4
+
5
+ // Encryption utilities
6
+ export { generateKey, encrypt, decrypt, encryptLog, decryptLog } from "./encrypt.js";
7
+
8
+ // Re-export transports for convenience
9
+ export { default as pinoTransport } from "./transports/pino.js";
10
+ export type { PinoTransportOptions } from "./transports/pino.js";
11
+
12
+ export { winstonTransport, AbbacchioWinstonTransport } from "./transports/winston.js";
13
+ export type { WinstonTransportOptions } from "./transports/winston.js";
14
+
15
+ export { bunyanStream, AbbacchioBunyanStream } from "./transports/bunyan.js";
16
+ export type { BunyanStreamOptions } from "./transports/bunyan.js";
17
+
18
+ export { interceptConsole, restoreConsole, getActiveClient } from "./transports/console.js";
19
+ export type { ConsoleInterceptorOptions } from "./transports/console.js";
@@ -0,0 +1,99 @@
1
+ import { Writable } from "stream";
2
+ import { AbbacchioClient, type AbbacchioClientOptions } from "../client.js";
3
+
4
+ export interface BunyanStreamOptions extends AbbacchioClientOptions {
5
+ /** Bunyan log level (optional) */
6
+ level?: number | string;
7
+ }
8
+
9
+ /**
10
+ * Bunyan stream for Abbacchio.
11
+ * Implements the Node.js Writable stream interface for Bunyan.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import bunyan from "bunyan";
16
+ * import { bunyanStream } from "@abbacchio/client/transports/bunyan";
17
+ *
18
+ * const logger = bunyan.createLogger({
19
+ * name: "myapp",
20
+ * streams: [
21
+ * { stream: process.stdout },
22
+ * bunyanStream({
23
+ * url: "http://localhost:4000/api/logs",
24
+ * channel: "my-app",
25
+ * secretKey: "optional-encryption-key",
26
+ * }),
27
+ * ],
28
+ * });
29
+ *
30
+ * logger.info("Hello from Bunyan!");
31
+ * ```
32
+ */
33
+ export class AbbacchioBunyanStream extends Writable {
34
+ private client: AbbacchioClient;
35
+ public level?: number | string;
36
+
37
+ constructor(opts: BunyanStreamOptions = {}) {
38
+ super({ objectMode: true });
39
+ this.client = new AbbacchioClient(opts);
40
+ this.level = opts.level;
41
+ }
42
+
43
+ /**
44
+ * Writable stream _write method - called for each log entry
45
+ */
46
+ _write(
47
+ chunk: Record<string, unknown>,
48
+ _encoding: BufferEncoding,
49
+ callback: (error?: Error | null) => void
50
+ ): void {
51
+ try {
52
+ // Transform Bunyan format to Abbacchio format
53
+ const log = this.transformLog(chunk);
54
+ this.client.add(log);
55
+ callback();
56
+ } catch (err) {
57
+ callback(err as Error);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Transform Bunyan log format to a normalized format
63
+ */
64
+ private transformLog(record: Record<string, unknown>): Record<string, unknown> {
65
+ const { name, hostname, pid, level, msg, time, v, ...rest } = record;
66
+
67
+ return {
68
+ level: level as number,
69
+ msg,
70
+ time: time ? new Date(time as string).getTime() : Date.now(),
71
+ name,
72
+ hostname,
73
+ pid,
74
+ ...rest,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Close the stream
80
+ */
81
+ _final(callback: (error?: Error | null) => void): void {
82
+ this.client.close().then(() => callback()).catch(callback);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Factory function to create a Bunyan stream
88
+ * Returns an object with stream and optional level for Bunyan's streams array
89
+ */
90
+ export function bunyanStream(opts?: BunyanStreamOptions): { stream: AbbacchioBunyanStream; level?: number | string; type: "raw" } {
91
+ const stream = new AbbacchioBunyanStream(opts);
92
+ return {
93
+ stream,
94
+ level: opts?.level,
95
+ type: "raw",
96
+ };
97
+ }
98
+
99
+ export default bunyanStream;
@@ -0,0 +1,147 @@
1
+ import { AbbacchioClient, type AbbacchioClientOptions } from "../client.js";
2
+
3
+ export interface ConsoleInterceptorOptions extends AbbacchioClientOptions {
4
+ /** Which console methods to intercept. Defaults to all. */
5
+ methods?: ("log" | "info" | "warn" | "error" | "debug")[];
6
+ /** Whether to still output to original console. Defaults to true. */
7
+ passthrough?: boolean;
8
+ }
9
+
10
+ type ConsoleMethod = "log" | "info" | "warn" | "error" | "debug";
11
+
12
+ const methodToLevel: Record<ConsoleMethod, number> = {
13
+ debug: 20,
14
+ log: 30,
15
+ info: 30,
16
+ warn: 40,
17
+ error: 50,
18
+ };
19
+
20
+ /**
21
+ * Console interceptor for Abbacchio.
22
+ * Intercepts console.log/info/warn/error/debug calls and sends them to Abbacchio.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { interceptConsole, restoreConsole } from "@abbacchio/client/transports/console";
27
+ *
28
+ * // Start intercepting console calls
29
+ * interceptConsole({
30
+ * url: "http://localhost:4000/api/logs",
31
+ * channel: "my-app",
32
+ * secretKey: "optional-encryption-key",
33
+ * passthrough: true, // Still log to console
34
+ * });
35
+ *
36
+ * console.log("This will be sent to Abbacchio!");
37
+ * console.error("Errors too!");
38
+ *
39
+ * // Stop intercepting when done
40
+ * restoreConsole();
41
+ * ```
42
+ */
43
+
44
+ // Store original console methods
45
+ const originalConsole: Record<ConsoleMethod, (...args: unknown[]) => void> = {
46
+ log: console.log.bind(console),
47
+ info: console.info.bind(console),
48
+ warn: console.warn.bind(console),
49
+ error: console.error.bind(console),
50
+ debug: console.debug.bind(console),
51
+ };
52
+
53
+ let activeClient: AbbacchioClient | null = null;
54
+ let activeOptions: ConsoleInterceptorOptions | null = null;
55
+
56
+ /**
57
+ * Format console arguments into a message string
58
+ */
59
+ function formatArgs(args: unknown[]): string {
60
+ return args
61
+ .map((arg) => {
62
+ if (typeof arg === "string") return arg;
63
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`;
64
+ try {
65
+ return JSON.stringify(arg);
66
+ } catch {
67
+ return String(arg);
68
+ }
69
+ })
70
+ .join(" ");
71
+ }
72
+
73
+ /**
74
+ * Create an intercepted console method
75
+ */
76
+ function createInterceptedMethod(
77
+ method: ConsoleMethod,
78
+ client: AbbacchioClient,
79
+ passthrough: boolean
80
+ ): (...args: unknown[]) => void {
81
+ return (...args: unknown[]) => {
82
+ // Send to Abbacchio
83
+ const log = {
84
+ level: methodToLevel[method],
85
+ msg: formatArgs(args),
86
+ time: Date.now(),
87
+ method,
88
+ };
89
+ client.add(log);
90
+
91
+ // Optionally pass through to original console
92
+ if (passthrough) {
93
+ originalConsole[method](...args);
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Start intercepting console calls
100
+ */
101
+ export function interceptConsole(opts: ConsoleInterceptorOptions = {}): void {
102
+ // Restore any existing interception first
103
+ if (activeClient) {
104
+ restoreConsole();
105
+ }
106
+
107
+ const methods = opts.methods || ["log", "info", "warn", "error", "debug"];
108
+ const passthrough = opts.passthrough !== false;
109
+
110
+ activeClient = new AbbacchioClient(opts);
111
+ activeOptions = opts;
112
+
113
+ // Replace console methods
114
+ for (const method of methods) {
115
+ (console as unknown as Record<string, unknown>)[method] = createInterceptedMethod(
116
+ method,
117
+ activeClient,
118
+ passthrough
119
+ );
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Stop intercepting console calls and restore original behavior
125
+ */
126
+ export function restoreConsole(): void {
127
+ // Restore original console methods
128
+ for (const method of Object.keys(originalConsole) as ConsoleMethod[]) {
129
+ (console as unknown as Record<string, unknown>)[method] = originalConsole[method];
130
+ }
131
+
132
+ // Flush and close client
133
+ if (activeClient) {
134
+ activeClient.close();
135
+ activeClient = null;
136
+ activeOptions = null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get the active client (for testing)
142
+ */
143
+ export function getActiveClient(): AbbacchioClient | null {
144
+ return activeClient;
145
+ }
146
+
147
+ export default interceptConsole;
@@ -0,0 +1,15 @@
1
+ // Pino transport
2
+ export { default as pinoTransport } from "./pino.js";
3
+ export type { PinoTransportOptions } from "./pino.js";
4
+
5
+ // Winston transport
6
+ export { winstonTransport, AbbacchioWinstonTransport } from "./winston.js";
7
+ export type { WinstonTransportOptions } from "./winston.js";
8
+
9
+ // Bunyan stream
10
+ export { bunyanStream, AbbacchioBunyanStream } from "./bunyan.js";
11
+ export type { BunyanStreamOptions } from "./bunyan.js";
12
+
13
+ // Console interceptor
14
+ export { interceptConsole, restoreConsole, getActiveClient } from "./console.js";
15
+ export type { ConsoleInterceptorOptions } from "./console.js";
@@ -0,0 +1,49 @@
1
+ import build from "pino-abstract-transport";
2
+ import { AbbacchioClient, type AbbacchioClientOptions } from "../client.js";
3
+
4
+ export interface PinoTransportOptions extends AbbacchioClientOptions {}
5
+
6
+ /**
7
+ * Pino transport for Abbacchio.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import pino from "pino";
12
+ *
13
+ * const logger = pino({
14
+ * transport: {
15
+ * target: "@abbacchio/client/transports/pino",
16
+ * options: {
17
+ * url: "http://localhost:4000/api/logs",
18
+ * channel: "my-app",
19
+ * secretKey: "optional-encryption-key",
20
+ * },
21
+ * },
22
+ * });
23
+ *
24
+ * logger.info("Hello from Pino!");
25
+ * ```
26
+ */
27
+ export default async function pinoTransport(opts: PinoTransportOptions = {}) {
28
+ const client = new AbbacchioClient(opts);
29
+
30
+ return build(
31
+ async function (source) {
32
+ for await (const obj of source) {
33
+ client.add(obj);
34
+ }
35
+ // Flush remaining on close
36
+ await client.flush();
37
+ },
38
+ {
39
+ async close() {
40
+ await client.close();
41
+ },
42
+ }
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Named export for programmatic usage
48
+ */
49
+ export { pinoTransport };
@@ -0,0 +1,100 @@
1
+ import TransportStream from "winston-transport";
2
+ import { AbbacchioClient, type AbbacchioClientOptions } from "../client.js";
3
+
4
+ export interface WinstonTransportOptions extends AbbacchioClientOptions {
5
+ /** Winston log level (optional) */
6
+ level?: string;
7
+ }
8
+
9
+ /**
10
+ * Winston transport for Abbacchio.
11
+ * Extends winston-transport for proper integration.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import winston from "winston";
16
+ * import { AbbacchioWinstonTransport } from "@abbacchio/client/transports/winston";
17
+ *
18
+ * const logger = winston.createLogger({
19
+ * transports: [
20
+ * new winston.transports.Console(),
21
+ * new AbbacchioWinstonTransport({
22
+ * url: "http://localhost:4000/api/logs",
23
+ * channel: "my-app",
24
+ * secretKey: "optional-encryption-key",
25
+ * }),
26
+ * ],
27
+ * });
28
+ *
29
+ * logger.info("Hello from Winston!");
30
+ * ```
31
+ */
32
+ export class AbbacchioWinstonTransport extends TransportStream {
33
+ private client: AbbacchioClient;
34
+
35
+ constructor(opts: WinstonTransportOptions = {}) {
36
+ super({ level: opts.level });
37
+ this.client = new AbbacchioClient(opts);
38
+ }
39
+
40
+ /**
41
+ * Winston log method - called for each log entry
42
+ */
43
+ log(info: Record<string, unknown>, callback: () => void): void {
44
+ setImmediate(() => {
45
+ this.emit("logged", info);
46
+ });
47
+
48
+ // Transform Winston format to Abbacchio format
49
+ const log = this.transformLog(info);
50
+ this.client.add(log);
51
+
52
+ callback();
53
+ }
54
+
55
+ /**
56
+ * Transform Winston log format to a normalized format
57
+ */
58
+ private transformLog(info: Record<string, unknown>): Record<string, unknown> {
59
+ const { level, message, timestamp, ...rest } = info;
60
+
61
+ return {
62
+ level: this.levelToNumber(level as string),
63
+ msg: message,
64
+ time: timestamp ? new Date(timestamp as string).getTime() : Date.now(),
65
+ ...rest,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Convert Winston level string to Pino-style number
71
+ */
72
+ private levelToNumber(level: string): number {
73
+ const levels: Record<string, number> = {
74
+ error: 50,
75
+ warn: 40,
76
+ info: 30,
77
+ http: 30,
78
+ verbose: 20,
79
+ debug: 20,
80
+ silly: 10,
81
+ };
82
+ return levels[level] || 30;
83
+ }
84
+
85
+ /**
86
+ * Close the transport
87
+ */
88
+ close(): void {
89
+ this.client.close();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Factory function to create a Winston transport
95
+ */
96
+ export function winstonTransport(opts?: WinstonTransportOptions): AbbacchioWinstonTransport {
97
+ return new AbbacchioWinstonTransport(opts);
98
+ }
99
+
100
+ export default winstonTransport;
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }