@checkstack/healthcheck-ssh-backend 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # @checkstack/healthcheck-ssh-backend
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/backend-api@0.0.2
10
+ - @checkstack/common@0.0.2
11
+ - @checkstack/healthcheck-common@0.0.2
12
+
13
+ ## 0.0.3
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [b4eb432]
18
+ - Updated dependencies [a65e002]
19
+ - @checkstack/backend-api@1.1.0
20
+ - @checkstack/common@0.2.0
21
+ - @checkstack/healthcheck-common@0.1.1
22
+
23
+ ## 0.0.2
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [ffc28f6]
28
+ - Updated dependencies [4dd644d]
29
+ - Updated dependencies [71275dd]
30
+ - Updated dependencies [ae19ff6]
31
+ - Updated dependencies [0babb9c]
32
+ - Updated dependencies [b55fae6]
33
+ - Updated dependencies [b354ab3]
34
+ - Updated dependencies [81f3f85]
35
+ - @checkstack/common@0.1.0
36
+ - @checkstack/backend-api@1.0.0
37
+ - @checkstack/healthcheck-common@0.1.0
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@checkstack/healthcheck-ssh-backend",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "lint": "bun run lint:code",
9
+ "lint:code": "eslint . --max-warnings 0"
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/backend-api": "workspace:*",
13
+ "@checkstack/common": "workspace:*",
14
+ "@checkstack/healthcheck-common": "workspace:*",
15
+ "ssh2": "^1.15.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "^1.0.0",
19
+ "@types/ssh2": "^1.15.0",
20
+ "typescript": "^5.0.0",
21
+ "@checkstack/tsconfig": "workspace:*",
22
+ "@checkstack/scripts": "workspace:*"
23
+ }
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ createBackendPlugin,
3
+ coreServices,
4
+ } from "@checkstack/backend-api";
5
+ import { SshHealthCheckStrategy } from "./strategy";
6
+ import { pluginMetadata } from "./plugin-metadata";
7
+
8
+ export default createBackendPlugin({
9
+ metadata: pluginMetadata,
10
+ register(env) {
11
+ env.registerInit({
12
+ deps: {
13
+ healthCheckRegistry: coreServices.healthCheckRegistry,
14
+ logger: coreServices.logger,
15
+ },
16
+ init: async ({ healthCheckRegistry, logger }) => {
17
+ logger.debug("🔌 Registering SSH Health Check Strategy...");
18
+ const strategy = new SshHealthCheckStrategy();
19
+ healthCheckRegistry.register(strategy);
20
+ },
21
+ });
22
+ },
23
+ });
@@ -0,0 +1,8 @@
1
+ import { definePluginMetadata } from "@checkstack/common";
2
+
3
+ /**
4
+ * Plugin metadata for the SSH Health Check backend.
5
+ */
6
+ export const pluginMetadata = definePluginMetadata({
7
+ pluginId: "healthcheck-ssh",
8
+ });
@@ -0,0 +1,245 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import { SshHealthCheckStrategy, SshClient } from "./strategy";
3
+
4
+ describe("SshHealthCheckStrategy", () => {
5
+ // Helper to create mock SSH client
6
+ const createMockClient = (
7
+ config: {
8
+ exitCode?: number;
9
+ stdout?: string;
10
+ stderr?: string;
11
+ execError?: Error;
12
+ connectError?: Error;
13
+ } = {}
14
+ ): SshClient => ({
15
+ connect: mock(() =>
16
+ config.connectError
17
+ ? Promise.reject(config.connectError)
18
+ : Promise.resolve({
19
+ exec: mock(() =>
20
+ config.execError
21
+ ? Promise.reject(config.execError)
22
+ : Promise.resolve({
23
+ exitCode: config.exitCode ?? 0,
24
+ stdout: config.stdout ?? "",
25
+ stderr: config.stderr ?? "",
26
+ })
27
+ ),
28
+ end: mock(() => {}),
29
+ })
30
+ ),
31
+ });
32
+
33
+ describe("execute", () => {
34
+ it("should return healthy for successful connection", async () => {
35
+ const strategy = new SshHealthCheckStrategy(createMockClient());
36
+
37
+ const result = await strategy.execute({
38
+ host: "localhost",
39
+ port: 22,
40
+ username: "user",
41
+ password: "secret",
42
+ timeout: 5000,
43
+ });
44
+
45
+ expect(result.status).toBe("healthy");
46
+ expect(result.metadata?.connected).toBe(true);
47
+ });
48
+
49
+ it("should return healthy for successful command execution", async () => {
50
+ const strategy = new SshHealthCheckStrategy(
51
+ createMockClient({ exitCode: 0, stdout: "OK" })
52
+ );
53
+
54
+ const result = await strategy.execute({
55
+ host: "localhost",
56
+ port: 22,
57
+ username: "user",
58
+ password: "secret",
59
+ timeout: 5000,
60
+ command: "echo OK",
61
+ });
62
+
63
+ expect(result.status).toBe("healthy");
64
+ expect(result.metadata?.commandSuccess).toBe(true);
65
+ expect(result.metadata?.stdout).toBe("OK");
66
+ expect(result.metadata?.exitCode).toBe(0);
67
+ });
68
+
69
+ it("should return unhealthy for connection error", async () => {
70
+ const strategy = new SshHealthCheckStrategy(
71
+ createMockClient({ connectError: new Error("Connection refused") })
72
+ );
73
+
74
+ const result = await strategy.execute({
75
+ host: "localhost",
76
+ port: 22,
77
+ username: "user",
78
+ password: "secret",
79
+ timeout: 5000,
80
+ });
81
+
82
+ expect(result.status).toBe("unhealthy");
83
+ expect(result.message).toContain("Connection refused");
84
+ expect(result.metadata?.connected).toBe(false);
85
+ });
86
+
87
+ it("should return unhealthy for non-zero exit code", async () => {
88
+ const strategy = new SshHealthCheckStrategy(
89
+ createMockClient({ exitCode: 1, stderr: "Error" })
90
+ );
91
+
92
+ const result = await strategy.execute({
93
+ host: "localhost",
94
+ port: 22,
95
+ username: "user",
96
+ password: "secret",
97
+ timeout: 5000,
98
+ command: "exit 1",
99
+ });
100
+
101
+ expect(result.status).toBe("unhealthy");
102
+ expect(result.metadata?.exitCode).toBe(1);
103
+ expect(result.metadata?.commandSuccess).toBe(false);
104
+ });
105
+
106
+ it("should pass connectionTime assertion when below threshold", async () => {
107
+ const strategy = new SshHealthCheckStrategy(createMockClient());
108
+
109
+ const result = await strategy.execute({
110
+ host: "localhost",
111
+ port: 22,
112
+ username: "user",
113
+ password: "secret",
114
+ timeout: 5000,
115
+ assertions: [
116
+ { field: "connectionTime", operator: "lessThan", value: 5000 },
117
+ ],
118
+ });
119
+
120
+ expect(result.status).toBe("healthy");
121
+ });
122
+
123
+ it("should pass exitCode assertion", async () => {
124
+ const strategy = new SshHealthCheckStrategy(
125
+ createMockClient({ exitCode: 0 })
126
+ );
127
+
128
+ const result = await strategy.execute({
129
+ host: "localhost",
130
+ port: 22,
131
+ username: "user",
132
+ password: "secret",
133
+ timeout: 5000,
134
+ command: "true",
135
+ assertions: [{ field: "exitCode", operator: "equals", value: 0 }],
136
+ });
137
+
138
+ expect(result.status).toBe("healthy");
139
+ });
140
+
141
+ it("should fail exitCode assertion when non-zero", async () => {
142
+ const strategy = new SshHealthCheckStrategy(
143
+ createMockClient({ exitCode: 1 })
144
+ );
145
+
146
+ const result = await strategy.execute({
147
+ host: "localhost",
148
+ port: 22,
149
+ username: "user",
150
+ password: "secret",
151
+ timeout: 5000,
152
+ command: "false",
153
+ assertions: [{ field: "exitCode", operator: "equals", value: 0 }],
154
+ });
155
+
156
+ expect(result.status).toBe("unhealthy");
157
+ expect(result.message).toContain("Assertion failed");
158
+ });
159
+
160
+ it("should pass stdout assertion", async () => {
161
+ const strategy = new SshHealthCheckStrategy(
162
+ createMockClient({ stdout: "OK: Service running" })
163
+ );
164
+
165
+ const result = await strategy.execute({
166
+ host: "localhost",
167
+ port: 22,
168
+ username: "user",
169
+ password: "secret",
170
+ timeout: 5000,
171
+ command: "systemctl status myservice",
172
+ assertions: [{ field: "stdout", operator: "contains", value: "OK" }],
173
+ });
174
+
175
+ expect(result.status).toBe("healthy");
176
+ });
177
+ });
178
+
179
+ describe("aggregateResult", () => {
180
+ it("should calculate averages correctly", () => {
181
+ const strategy = new SshHealthCheckStrategy();
182
+ const runs = [
183
+ {
184
+ id: "1",
185
+ status: "healthy" as const,
186
+ latencyMs: 100,
187
+ checkId: "c1",
188
+ timestamp: new Date(),
189
+ metadata: {
190
+ connected: true,
191
+ connectionTimeMs: 50,
192
+ commandTimeMs: 10,
193
+ exitCode: 0,
194
+ commandSuccess: true,
195
+ },
196
+ },
197
+ {
198
+ id: "2",
199
+ status: "healthy" as const,
200
+ latencyMs: 150,
201
+ checkId: "c1",
202
+ timestamp: new Date(),
203
+ metadata: {
204
+ connected: true,
205
+ connectionTimeMs: 100,
206
+ commandTimeMs: 20,
207
+ exitCode: 0,
208
+ commandSuccess: true,
209
+ },
210
+ },
211
+ ];
212
+
213
+ const aggregated = strategy.aggregateResult(runs);
214
+
215
+ expect(aggregated.avgConnectionTime).toBe(75);
216
+ expect(aggregated.avgCommandTime).toBe(15);
217
+ expect(aggregated.successRate).toBe(100);
218
+ expect(aggregated.errorCount).toBe(0);
219
+ });
220
+
221
+ it("should count errors", () => {
222
+ const strategy = new SshHealthCheckStrategy();
223
+ const runs = [
224
+ {
225
+ id: "1",
226
+ status: "unhealthy" as const,
227
+ latencyMs: 100,
228
+ checkId: "c1",
229
+ timestamp: new Date(),
230
+ metadata: {
231
+ connected: false,
232
+ connectionTimeMs: 100,
233
+ commandSuccess: false,
234
+ error: "Connection refused",
235
+ },
236
+ },
237
+ ];
238
+
239
+ const aggregated = strategy.aggregateResult(runs);
240
+
241
+ expect(aggregated.errorCount).toBe(1);
242
+ expect(aggregated.successRate).toBe(0);
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,403 @@
1
+ import { Client } from "ssh2";
2
+ import {
3
+ HealthCheckStrategy,
4
+ HealthCheckResult,
5
+ HealthCheckRunForAggregation,
6
+ Versioned,
7
+ z,
8
+ timeThresholdField,
9
+ numericField,
10
+ booleanField,
11
+ stringField,
12
+ evaluateAssertions,
13
+ configString,
14
+ configNumber,
15
+ } from "@checkstack/backend-api";
16
+ import {
17
+ healthResultBoolean,
18
+ healthResultNumber,
19
+ healthResultString,
20
+ } from "@checkstack/healthcheck-common";
21
+
22
+ // ============================================================================
23
+ // SCHEMAS
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Assertion schema for SSH health checks using shared factories.
28
+ */
29
+ const sshAssertionSchema = z.discriminatedUnion("field", [
30
+ timeThresholdField("connectionTime"),
31
+ timeThresholdField("commandTime"),
32
+ numericField("exitCode", { min: 0 }),
33
+ booleanField("commandSuccess"),
34
+ stringField("stdout"),
35
+ ]);
36
+
37
+ export type SshAssertion = z.infer<typeof sshAssertionSchema>;
38
+
39
+ /**
40
+ * Configuration schema for SSH health checks.
41
+ */
42
+ export const sshConfigSchema = z.object({
43
+ host: z.string().describe("SSH server hostname"),
44
+ port: z.number().int().min(1).max(65_535).default(22).describe("SSH port"),
45
+ username: z.string().describe("SSH username"),
46
+ password: configString({ "x-secret": true })
47
+ .describe("Password for authentication")
48
+ .optional(),
49
+ privateKey: configString({ "x-secret": true })
50
+ .describe("Private key for authentication")
51
+ .optional(),
52
+ passphrase: configString({ "x-secret": true })
53
+ .describe("Passphrase for private key")
54
+ .optional(),
55
+ timeout: configNumber({})
56
+ .min(100)
57
+ .default(10_000)
58
+ .describe("Connection timeout in milliseconds"),
59
+ command: configString({})
60
+ .optional()
61
+ .describe("Command to execute for health check (optional)"),
62
+ assertions: z
63
+ .array(sshAssertionSchema)
64
+ .optional()
65
+ .describe("Validation conditions"),
66
+ });
67
+
68
+ export type SshConfig = z.infer<typeof sshConfigSchema>;
69
+ export type SshConfigInput = z.input<typeof sshConfigSchema>;
70
+
71
+ /**
72
+ * Per-run result metadata.
73
+ */
74
+ const sshResultSchema = z.object({
75
+ connected: healthResultBoolean({
76
+ "x-chart-type": "boolean",
77
+ "x-chart-label": "Connected",
78
+ }),
79
+ connectionTimeMs: healthResultNumber({
80
+ "x-chart-type": "line",
81
+ "x-chart-label": "Connection Time",
82
+ "x-chart-unit": "ms",
83
+ }),
84
+ commandTimeMs: healthResultNumber({
85
+ "x-chart-type": "line",
86
+ "x-chart-label": "Command Time",
87
+ "x-chart-unit": "ms",
88
+ }).optional(),
89
+ exitCode: healthResultNumber({
90
+ "x-chart-type": "counter",
91
+ "x-chart-label": "Exit Code",
92
+ }).optional(),
93
+ stdout: healthResultString({
94
+ "x-chart-type": "text",
95
+ "x-chart-label": "Stdout",
96
+ }).optional(),
97
+ stderr: healthResultString({
98
+ "x-chart-type": "text",
99
+ "x-chart-label": "Stderr",
100
+ }).optional(),
101
+ commandSuccess: healthResultBoolean({
102
+ "x-chart-type": "boolean",
103
+ "x-chart-label": "Command Success",
104
+ }),
105
+ failedAssertion: sshAssertionSchema.optional(),
106
+ error: healthResultString({
107
+ "x-chart-type": "status",
108
+ "x-chart-label": "Error",
109
+ }).optional(),
110
+ });
111
+
112
+ export type SshResult = z.infer<typeof sshResultSchema>;
113
+
114
+ /**
115
+ * Aggregated metadata for buckets.
116
+ */
117
+ const sshAggregatedSchema = z.object({
118
+ avgConnectionTime: healthResultNumber({
119
+ "x-chart-type": "line",
120
+ "x-chart-label": "Avg Connection Time",
121
+ "x-chart-unit": "ms",
122
+ }),
123
+ avgCommandTime: healthResultNumber({
124
+ "x-chart-type": "line",
125
+ "x-chart-label": "Avg Command Time",
126
+ "x-chart-unit": "ms",
127
+ }),
128
+ successRate: healthResultNumber({
129
+ "x-chart-type": "gauge",
130
+ "x-chart-label": "Success Rate",
131
+ "x-chart-unit": "%",
132
+ }),
133
+ errorCount: healthResultNumber({
134
+ "x-chart-type": "counter",
135
+ "x-chart-label": "Errors",
136
+ }),
137
+ });
138
+
139
+ export type SshAggregatedResult = z.infer<typeof sshAggregatedSchema>;
140
+
141
+ // ============================================================================
142
+ // SSH CLIENT INTERFACE (for testability)
143
+ // ============================================================================
144
+
145
+ export interface SshCommandResult {
146
+ exitCode: number;
147
+ stdout: string;
148
+ stderr: string;
149
+ }
150
+
151
+ export interface SshConnection {
152
+ exec(command: string): Promise<SshCommandResult>;
153
+ end(): void;
154
+ }
155
+
156
+ export interface SshClient {
157
+ connect(config: {
158
+ host: string;
159
+ port: number;
160
+ username: string;
161
+ password?: string;
162
+ privateKey?: string;
163
+ passphrase?: string;
164
+ readyTimeout: number;
165
+ }): Promise<SshConnection>;
166
+ }
167
+
168
+ // Default client using ssh2
169
+ const defaultSshClient: SshClient = {
170
+ connect(config) {
171
+ return new Promise((resolve, reject) => {
172
+ const client = new Client();
173
+
174
+ client.on("ready", () => {
175
+ resolve({
176
+ exec(command: string): Promise<SshCommandResult> {
177
+ return new Promise((execResolve, execReject) => {
178
+ client.exec(command, (err, stream) => {
179
+ if (err) {
180
+ execReject(err);
181
+ return;
182
+ }
183
+
184
+ let stdout = "";
185
+ let stderr = "";
186
+
187
+ stream.on("data", (data: Buffer) => {
188
+ stdout += data.toString();
189
+ });
190
+
191
+ stream.stderr.on("data", (data: Buffer) => {
192
+ stderr += data.toString();
193
+ });
194
+
195
+ stream.on("close", (code: number | null) => {
196
+ execResolve({
197
+ exitCode: code ?? 0,
198
+ stdout: stdout.trim(),
199
+ stderr: stderr.trim(),
200
+ });
201
+ });
202
+
203
+ stream.on("error", execReject);
204
+ });
205
+ });
206
+ },
207
+ end() {
208
+ client.end();
209
+ },
210
+ });
211
+ });
212
+
213
+ client.on("error", reject);
214
+
215
+ client.connect({
216
+ host: config.host,
217
+ port: config.port,
218
+ username: config.username,
219
+ password: config.password,
220
+ privateKey: config.privateKey,
221
+ passphrase: config.passphrase,
222
+ readyTimeout: config.readyTimeout,
223
+ });
224
+ });
225
+ },
226
+ };
227
+
228
+ // ============================================================================
229
+ // STRATEGY
230
+ // ============================================================================
231
+
232
+ export class SshHealthCheckStrategy
233
+ implements HealthCheckStrategy<SshConfig, SshResult, SshAggregatedResult>
234
+ {
235
+ id = "ssh";
236
+ displayName = "SSH Health Check";
237
+ description = "SSH server connectivity and command execution health check";
238
+
239
+ private sshClient: SshClient;
240
+
241
+ constructor(sshClient: SshClient = defaultSshClient) {
242
+ this.sshClient = sshClient;
243
+ }
244
+
245
+ config: Versioned<SshConfig> = new Versioned({
246
+ version: 1,
247
+ schema: sshConfigSchema,
248
+ });
249
+
250
+ result: Versioned<SshResult> = new Versioned({
251
+ version: 1,
252
+ schema: sshResultSchema,
253
+ });
254
+
255
+ aggregatedResult: Versioned<SshAggregatedResult> = new Versioned({
256
+ version: 1,
257
+ schema: sshAggregatedSchema,
258
+ });
259
+
260
+ aggregateResult(
261
+ runs: HealthCheckRunForAggregation<SshResult>[]
262
+ ): SshAggregatedResult {
263
+ let totalConnectionTime = 0;
264
+ let totalCommandTime = 0;
265
+ let successCount = 0;
266
+ let errorCount = 0;
267
+ let validRuns = 0;
268
+ let commandRuns = 0;
269
+
270
+ for (const run of runs) {
271
+ if (run.metadata?.error) {
272
+ errorCount++;
273
+ continue;
274
+ }
275
+ if (run.status === "healthy") {
276
+ successCount++;
277
+ }
278
+ if (run.metadata) {
279
+ totalConnectionTime += run.metadata.connectionTimeMs;
280
+ if (run.metadata.commandTimeMs !== undefined) {
281
+ totalCommandTime += run.metadata.commandTimeMs;
282
+ commandRuns++;
283
+ }
284
+ validRuns++;
285
+ }
286
+ }
287
+
288
+ return {
289
+ avgConnectionTime: validRuns > 0 ? totalConnectionTime / validRuns : 0,
290
+ avgCommandTime: commandRuns > 0 ? totalCommandTime / commandRuns : 0,
291
+ successRate: runs.length > 0 ? (successCount / runs.length) * 100 : 0,
292
+ errorCount,
293
+ };
294
+ }
295
+
296
+ async execute(config: SshConfigInput): Promise<HealthCheckResult<SshResult>> {
297
+ const validatedConfig = this.config.validate(config);
298
+ const start = performance.now();
299
+
300
+ try {
301
+ // Connect to SSH server
302
+ const connection = await this.sshClient.connect({
303
+ host: validatedConfig.host,
304
+ port: validatedConfig.port,
305
+ username: validatedConfig.username,
306
+ password: validatedConfig.password,
307
+ privateKey: validatedConfig.privateKey,
308
+ passphrase: validatedConfig.passphrase,
309
+ readyTimeout: validatedConfig.timeout,
310
+ });
311
+
312
+ const connectionTimeMs = Math.round(performance.now() - start);
313
+
314
+ let commandTimeMs: number | undefined;
315
+ let exitCode: number | undefined;
316
+ let stdout: string | undefined;
317
+ let stderr: string | undefined;
318
+ let commandSuccess = true;
319
+
320
+ // Execute command if provided
321
+ if (validatedConfig.command) {
322
+ const commandStart = performance.now();
323
+ try {
324
+ const result = await connection.exec(validatedConfig.command);
325
+ exitCode = result.exitCode;
326
+ stdout = result.stdout;
327
+ stderr = result.stderr;
328
+ commandSuccess = result.exitCode === 0;
329
+ commandTimeMs = Math.round(performance.now() - commandStart);
330
+ } catch {
331
+ commandSuccess = false;
332
+ commandTimeMs = Math.round(performance.now() - commandStart);
333
+ }
334
+ }
335
+
336
+ connection.end();
337
+
338
+ const result: Omit<SshResult, "failedAssertion" | "error"> = {
339
+ connected: true,
340
+ connectionTimeMs,
341
+ commandTimeMs,
342
+ exitCode,
343
+ stdout,
344
+ stderr,
345
+ commandSuccess,
346
+ };
347
+
348
+ // Evaluate assertions using shared utility
349
+ const failedAssertion = evaluateAssertions(validatedConfig.assertions, {
350
+ connectionTime: connectionTimeMs,
351
+ commandTime: commandTimeMs ?? 0,
352
+ exitCode: exitCode ?? 0,
353
+ commandSuccess,
354
+ stdout: stdout ?? "",
355
+ });
356
+
357
+ if (failedAssertion) {
358
+ return {
359
+ status: "unhealthy",
360
+ latencyMs: connectionTimeMs + (commandTimeMs ?? 0),
361
+ message: `Assertion failed: ${failedAssertion.field} ${
362
+ failedAssertion.operator
363
+ }${"value" in failedAssertion ? ` ${failedAssertion.value}` : ""}`,
364
+ metadata: { ...result, failedAssertion },
365
+ };
366
+ }
367
+
368
+ if (!commandSuccess && validatedConfig.command) {
369
+ return {
370
+ status: "unhealthy",
371
+ latencyMs: connectionTimeMs + (commandTimeMs ?? 0),
372
+ message: `Command failed with exit code ${exitCode}`,
373
+ metadata: result,
374
+ };
375
+ }
376
+
377
+ const message = validatedConfig.command
378
+ ? `SSH connected, command executed (exit ${exitCode}) in ${commandTimeMs}ms`
379
+ : `SSH connected in ${connectionTimeMs}ms`;
380
+
381
+ return {
382
+ status: "healthy",
383
+ latencyMs: connectionTimeMs + (commandTimeMs ?? 0),
384
+ message,
385
+ metadata: result,
386
+ };
387
+ } catch (error: unknown) {
388
+ const end = performance.now();
389
+ const isError = error instanceof Error;
390
+ return {
391
+ status: "unhealthy",
392
+ latencyMs: Math.round(end - start),
393
+ message: isError ? error.message : "SSH connection failed",
394
+ metadata: {
395
+ connected: false,
396
+ connectionTimeMs: Math.round(end - start),
397
+ commandSuccess: false,
398
+ error: isError ? error.name : "UnknownError",
399
+ },
400
+ };
401
+ }
402
+ }
403
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }