@tmlmobilidade/ssh 20260709.1401.58 → 20260710.2205.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ import { type Server } from 'node:net';
2
+ import { type ForwardOptions, type ServerOptions, type SshOptions, type TunnelOptions } from 'tunnel-ssh';
3
+ export interface SshConfig {
4
+ forwardOptions: ForwardOptions;
5
+ serverOptions: ServerOptions;
6
+ sshOptions: SshOptions;
7
+ tunnelOptions: TunnelOptions;
8
+ }
9
+ export interface SshTunnelOptions {
10
+ maxRetries?: number;
11
+ }
12
+ export declare class SshTunnel {
13
+ private _server;
14
+ private config;
15
+ private options;
16
+ private retries;
17
+ constructor(config: SshConfig, options?: SshTunnelOptions);
18
+ get server(): Server | undefined;
19
+ /**
20
+ * Establishes an SSH tunnel connection using the provided configuration options.
21
+ * @throws Throws an error if the connection fails after the maximum number of retries.
22
+ * @remarks
23
+ * - The method attempts to create an SSH tunnel using the `createTunnel` function with the specified options.
24
+ * - If the connection is successful, it logs the connected host port and sets up an error listener on the server.
25
+ * - If the connection fails, it retries the connection up to a maximum number of retries specified in the options.
26
+ * @example ```typescript
27
+ * const SshTunnel = new SshTunnel(config);
28
+ * SshTunnel.connect();
29
+ * ```
30
+ */
31
+ connect(): any;
32
+ /**
33
+ * Disconnects the SSH tunnel by closing the server.
34
+ * @returns A promise that resolves when the server is successfully closed.
35
+ * @throws Will log an error message if the server fails to close.
36
+ */
37
+ disconnect(): Promise<void>;
38
+ /**
39
+ * Reconnects the SSH tunnel by first disconnecting and then connecting again.
40
+ * This method ensures that the connection is reset.
41
+ * @returns A promise that resolves when the reconnection process is complete.
42
+ */
43
+ reconnect(): Promise<void>;
44
+ }
package/dist/client.js ADDED
@@ -0,0 +1,88 @@
1
+ /* * */
2
+ import { Logger } from '@tmlmobilidade/logger';
3
+ import { createTunnel } from 'tunnel-ssh';
4
+ /* * */
5
+ export class SshTunnel {
6
+ _server;
7
+ config;
8
+ options;
9
+ retries = 0;
10
+ constructor(config, options) {
11
+ this.config = config;
12
+ if (options)
13
+ this.options = options;
14
+ }
15
+ get server() {
16
+ return this._server;
17
+ }
18
+ /**
19
+ * Establishes an SSH tunnel connection using the provided configuration options.
20
+ * @throws Throws an error if the connection fails after the maximum number of retries.
21
+ * @remarks
22
+ * - The method attempts to create an SSH tunnel using the `createTunnel` function with the specified options.
23
+ * - If the connection is successful, it logs the connected host port and sets up an error listener on the server.
24
+ * - If the connection fails, it retries the connection up to a maximum number of retries specified in the options.
25
+ * @example ```typescript
26
+ * const SshTunnel = new SshTunnel(config);
27
+ * SshTunnel.connect();
28
+ * ```
29
+ */
30
+ async connect() {
31
+ try {
32
+ if (this._server) {
33
+ // If the server is already connected, return it
34
+ console.log(`⤷ SSH Tunnel already connected.`);
35
+ return this._server;
36
+ }
37
+ const [server] = await createTunnel(this.config.tunnelOptions, this.config.serverOptions, this.config.sshOptions, this.config.forwardOptions);
38
+ Logger.info({ message: `SSH Tunnel connected to host port ${server.address().port}` });
39
+ this._server = server;
40
+ server.on('error', (error) => {
41
+ Logger.error({ error, message: 'SSH Tunnel Error' });
42
+ });
43
+ server.on('close', () => {
44
+ Logger.info({ message: 'SSH Tunnel closed.' });
45
+ });
46
+ return this._server;
47
+ }
48
+ catch (error) {
49
+ if (error.code === 'EADDRINUSE') {
50
+ Logger.info({ message: `Port "${this.config.serverOptions.port}" already in use. Retrying with a different port...` });
51
+ this.config.serverOptions.port++;
52
+ return await this.connect();
53
+ }
54
+ else if (this.retries < (this.options?.maxRetries || 3)) {
55
+ Logger.error({ error, message: 'Failed to connect to SSH Tunnel.' });
56
+ this.retries++;
57
+ Logger.info({ message: 'Retrying SSH connection...' });
58
+ return await this.connect();
59
+ }
60
+ else {
61
+ throw new Error('Error connecting to SSH tunnel', { cause: error });
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Disconnects the SSH tunnel by closing the server.
67
+ * @returns A promise that resolves when the server is successfully closed.
68
+ * @throws Will log an error message if the server fails to close.
69
+ */
70
+ async disconnect() {
71
+ try {
72
+ this._server.close();
73
+ console.log(`⤷ SSH Tunnel disconnected.`);
74
+ }
75
+ catch (error) {
76
+ console.log(`⤷ ERROR: Failed to disconnect from SSH Tunnel.`, error);
77
+ }
78
+ }
79
+ /**
80
+ * Reconnects the SSH tunnel by first disconnecting and then connecting again.
81
+ * This method ensures that the connection is reset.
82
+ * @returns A promise that resolves when the reconnection process is complete.
83
+ */
84
+ async reconnect() {
85
+ await this.disconnect();
86
+ await this.connect();
87
+ }
88
+ }
package/dist/index.d.ts CHANGED
@@ -1,49 +1,3 @@
1
- import { type Server } from 'node:net';
2
- import { type ForwardOptions, type ServerOptions, type SshOptions, type TunnelOptions } from 'tunnel-ssh';
3
- export interface SshConfig {
4
- forwardOptions: ForwardOptions;
5
- serverOptions: ServerOptions;
6
- sshOptions: SshOptions;
7
- tunnelOptions: TunnelOptions;
8
- }
9
- export interface SshTunnelServiceOptions {
10
- maxRetries?: number;
11
- }
12
- export declare class SshTunnelService {
13
- private static _instance;
14
- private _server;
15
- private config;
16
- private options;
17
- private retries;
18
- constructor(config: SshConfig, options?: SshTunnelServiceOptions);
19
- get server(): Server | undefined;
20
- /**
21
- * Get the singleton instance of SshTunnelService.
22
- */
23
- static getInstance(config?: SshConfig, options?: SshTunnelServiceOptions): SshTunnelService;
24
- /**
25
- * Establishes an SSH tunnel connection using the provided configuration options.
26
- * @throws Throws an error if the connection fails after the maximum number of retries.
27
- * @remarks
28
- * - The method attempts to create an SSH tunnel using the `createTunnel` function with the specified options.
29
- * - If the connection is successful, it logs the connected host port and sets up an error listener on the server.
30
- * - If the connection fails, it retries the connection up to a maximum number of retries specified in the options.
31
- * @example ```typescript
32
- * const sshTunnelService = new SshTunnelService(config);
33
- * sshTunnelService.connect();
34
- * ```
35
- */
36
- connect(): any;
37
- /**
38
- * Disconnects the SSH tunnel by closing the server.
39
- * @returns A promise that resolves when the server is successfully closed.
40
- * @throws Will log an error message if the server fails to close.
41
- */
42
- disconnect(): Promise<void>;
43
- /**
44
- * Reconnects the SSH tunnel by first disconnecting and then connecting again.
45
- * This method ensures that the connection is reset.
46
- * @returns A promise that resolves when the reconnection process is complete.
47
- */
48
- reconnect(): Promise<void>;
49
- }
1
+ export * from './client.js';
2
+ export * from './ssh.factory.js';
3
+ export * from './tunnels.js';
package/dist/index.js CHANGED
@@ -1,101 +1,3 @@
1
- /* * */
2
- import { Logger } from '@tmlmobilidade/logger';
3
- import { createTunnel } from 'tunnel-ssh';
4
- /* * */
5
- export class SshTunnelService {
6
- static _instance;
7
- _server;
8
- config;
9
- options;
10
- retries = 0;
11
- constructor(config, options) {
12
- this.config = config;
13
- if (options)
14
- this.options = options;
15
- }
16
- get server() {
17
- return this._server;
18
- }
19
- /**
20
- * Get the singleton instance of SshTunnelService.
21
- */
22
- static getInstance(config, options) {
23
- if (!SshTunnelService._instance) {
24
- if (!config) {
25
- throw new Error('SSH Config is required');
26
- }
27
- SshTunnelService._instance = new SshTunnelService(config, options);
28
- }
29
- return SshTunnelService._instance;
30
- }
31
- /**
32
- * Establishes an SSH tunnel connection using the provided configuration options.
33
- * @throws Throws an error if the connection fails after the maximum number of retries.
34
- * @remarks
35
- * - The method attempts to create an SSH tunnel using the `createTunnel` function with the specified options.
36
- * - If the connection is successful, it logs the connected host port and sets up an error listener on the server.
37
- * - If the connection fails, it retries the connection up to a maximum number of retries specified in the options.
38
- * @example ```typescript
39
- * const sshTunnelService = new SshTunnelService(config);
40
- * sshTunnelService.connect();
41
- * ```
42
- */
43
- async connect() {
44
- try {
45
- if (this._server) {
46
- // If the server is already connected, return it
47
- console.log(`⤷ SSH Tunnel already connected.`);
48
- return this._server;
49
- }
50
- const [server] = await createTunnel(this.config.tunnelOptions, this.config.serverOptions, this.config.sshOptions, this.config.forwardOptions);
51
- Logger.info({ message: `SSH Tunnel connected to host port ${server.address().port}` });
52
- this._server = server;
53
- server.on('error', (error) => {
54
- Logger.error({ error, message: 'SSH Tunnel Error' });
55
- });
56
- server.on('close', () => {
57
- Logger.info({ message: 'SSH Tunnel closed.' });
58
- });
59
- return this._server;
60
- }
61
- catch (error) {
62
- if (error.code === 'EADDRINUSE') {
63
- Logger.info({ message: `Port "${this.config.serverOptions.port}" already in use. Retrying with a different port...` });
64
- this.config.serverOptions.port++;
65
- return await this.connect();
66
- }
67
- else if (this.retries < (this.options?.maxRetries || 3)) {
68
- Logger.error({ error, message: 'Failed to connect to SSH Tunnel.' });
69
- this.retries++;
70
- Logger.info({ message: 'Retrying SSH connection...' });
71
- return await this.connect();
72
- }
73
- else {
74
- throw new Error('Error connecting to SSH tunnel', error);
75
- }
76
- }
77
- }
78
- /**
79
- * Disconnects the SSH tunnel by closing the server.
80
- * @returns A promise that resolves when the server is successfully closed.
81
- * @throws Will log an error message if the server fails to close.
82
- */
83
- async disconnect() {
84
- try {
85
- this._server.close();
86
- console.log(`⤷ SSH Tunnel disconnected.`);
87
- }
88
- catch (error) {
89
- console.log(`⤷ ERROR: Failed to disconnect from SSH Tunnel.`, error);
90
- }
91
- }
92
- /**
93
- * Reconnects the SSH tunnel by first disconnecting and then connecting again.
94
- * This method ensures that the connection is reset.
95
- * @returns A promise that resolves when the reconnection process is complete.
96
- */
97
- async reconnect() {
98
- await this.disconnect();
99
- this.connect();
100
- }
101
- }
1
+ export * from './client.js';
2
+ export * from './ssh.factory.js';
3
+ export * from './tunnels.js';
@@ -0,0 +1,26 @@
1
+ import { SshTunnel } from './client.js';
2
+ type SshTunnelType = 'GO' | 'PCGI';
3
+ interface SshTunnelFactoryOptions {
4
+ dstAddr: string;
5
+ dstPort: number;
6
+ maxRetries?: number;
7
+ }
8
+ export type SshTunnelFactory = (options: SshTunnelFactoryOptions) => null | SshTunnel;
9
+ /**
10
+ * Creates an SSH tunnel factory for the given type.
11
+ *
12
+ * The returned function reads `{type}_TUNNEL_*` environment variables
13
+ * and builds an `SshTunnel` when tunneling is enabled.
14
+ *
15
+ * Expected environment variables:
16
+ * `{type}_TUNNEL_ENABLED` — `"true"` or `"false"`; `"false"` returns `null`
17
+ * `{type}_TUNNEL_SSH_HOST`
18
+ * `{type}_TUNNEL_SSH_USERNAME`
19
+ * `{type}_TUNNEL_SSH_KEY_PATH` (optional)
20
+ * `{type}_TUNNEL_SSH_KEY` (optional)
21
+ * `SSH_AUTH_SOCK` (optional fallback agent)
22
+ *
23
+ * Auth priority: `TUNNEL_SSH_KEY_PATH` > `TUNNEL_SSH_KEY` > `SSH_AUTH_SOCK`.
24
+ */
25
+ export declare function createSshTunnelFactory(type: SshTunnelType): SshTunnelFactory;
26
+ export {};
@@ -0,0 +1,75 @@
1
+ /* * */
2
+ import { randomInt } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { SshTunnel } from './client.js';
5
+ /**
6
+ * Creates an SSH tunnel factory for the given type.
7
+ *
8
+ * The returned function reads `{type}_TUNNEL_*` environment variables
9
+ * and builds an `SshTunnel` when tunneling is enabled.
10
+ *
11
+ * Expected environment variables:
12
+ * `{type}_TUNNEL_ENABLED` — `"true"` or `"false"`; `"false"` returns `null`
13
+ * `{type}_TUNNEL_SSH_HOST`
14
+ * `{type}_TUNNEL_SSH_USERNAME`
15
+ * `{type}_TUNNEL_SSH_KEY_PATH` (optional)
16
+ * `{type}_TUNNEL_SSH_KEY` (optional)
17
+ * `SSH_AUTH_SOCK` (optional fallback agent)
18
+ *
19
+ * Auth priority: `TUNNEL_SSH_KEY_PATH` > `TUNNEL_SSH_KEY` > `SSH_AUTH_SOCK`.
20
+ */
21
+ export function createSshTunnelFactory(type) {
22
+ return (options) => buildSshTunnel(type, options);
23
+ }
24
+ function buildSshTunnel(type, options) {
25
+ const { dstAddr, dstPort, maxRetries } = options;
26
+ const env = (name) => process.env[`${type}_${name}`];
27
+ if (env('TUNNEL_ENABLED') !== 'true' && env('TUNNEL_ENABLED') !== 'false') {
28
+ throw new Error(`Missing ${type}_TUNNEL_ENABLED. Please indicate whether SSH tunneling is required by setting ${type}_TUNNEL_ENABLED to "true" or "false".`);
29
+ }
30
+ if (env('TUNNEL_ENABLED') === 'false') {
31
+ return null;
32
+ }
33
+ if (!env('TUNNEL_SSH_HOST')) {
34
+ throw new Error(`Missing ${type}_TUNNEL_SSH_HOST environment variable.`);
35
+ }
36
+ if (!env('TUNNEL_SSH_USERNAME')) {
37
+ throw new Error(`Missing ${type}_TUNNEL_SSH_USERNAME environment variable.`);
38
+ }
39
+ if (!env('TUNNEL_SSH_KEY_PATH') && !env('TUNNEL_SSH_KEY') && !process.env.SSH_AUTH_SOCK) {
40
+ throw new Error(`Missing authentication configuration. Please provide ${type}_TUNNEL_SSH_KEY_PATH, ${type}_TUNNEL_SSH_KEY, or ensure SSH_AUTH_SOCK is set.`);
41
+ }
42
+ const srcPort = randomInt(8_000, 8_999);
43
+ const sshConfig = {
44
+ forwardOptions: {
45
+ dstAddr: dstAddr,
46
+ dstPort: dstPort,
47
+ srcAddr: 'localhost',
48
+ srcPort: srcPort,
49
+ },
50
+ serverOptions: {
51
+ port: srcPort,
52
+ },
53
+ sshOptions: {
54
+ agent: (env('TUNNEL_SSH_KEY_PATH') || env('TUNNEL_SSH_KEY')) ? undefined : process.env.SSH_AUTH_SOCK,
55
+ host: env('TUNNEL_SSH_HOST'),
56
+ keepaliveCountMax: 3,
57
+ keepaliveInterval: 10_000,
58
+ port: 22,
59
+ privateKey: env('TUNNEL_SSH_KEY_PATH')
60
+ ? readFileSync(env('TUNNEL_SSH_KEY_PATH'))
61
+ : env('TUNNEL_SSH_KEY')
62
+ ? env('TUNNEL_SSH_KEY')
63
+ : undefined,
64
+ username: env('TUNNEL_SSH_USERNAME'),
65
+ },
66
+ tunnelOptions: {
67
+ autoClose: false,
68
+ reconnectOnError: true,
69
+ },
70
+ };
71
+ const sshOptions = {
72
+ maxRetries: maxRetries ?? 3,
73
+ };
74
+ return new SshTunnel(sshConfig, sshOptions);
75
+ }
@@ -0,0 +1,2 @@
1
+ export declare const goSshTunnel: import("./ssh.factory.js").SshTunnelFactory;
2
+ export declare const pcgiSshTunnel: import("./ssh.factory.js").SshTunnelFactory;
@@ -0,0 +1,5 @@
1
+ /* * */
2
+ import { createSshTunnelFactory } from './ssh.factory.js';
3
+ /* * */
4
+ export const goSshTunnel = createSshTunnelFactory('GO');
5
+ export const pcgiSshTunnel = createSshTunnelFactory('PCGI');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/ssh",
3
- "version": "20260709.1401.58",
3
+ "version": "20260710.2205.51",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"