@checkstack/healthcheck-tls-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-tls-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,22 @@
1
+ {
2
+ "name": "@checkstack/healthcheck-tls-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
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "^1.0.0",
18
+ "typescript": "^5.0.0",
19
+ "@checkstack/tsconfig": "workspace:*",
20
+ "@checkstack/scripts": "workspace:*"
21
+ }
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ createBackendPlugin,
3
+ coreServices,
4
+ } from "@checkstack/backend-api";
5
+ import { TlsHealthCheckStrategy } 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 TLS/SSL Health Check Strategy...");
18
+ const strategy = new TlsHealthCheckStrategy();
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 TLS/SSL Health Check backend.
5
+ */
6
+ export const pluginMetadata = definePluginMetadata({
7
+ pluginId: "healthcheck-tls",
8
+ });
@@ -0,0 +1,308 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import {
3
+ TlsHealthCheckStrategy,
4
+ TlsClient,
5
+ TlsConnection,
6
+ CertificateInfo,
7
+ } from "./strategy";
8
+
9
+ describe("TlsHealthCheckStrategy", () => {
10
+ // Create a valid certificate info (30 days until expiry)
11
+ const createCertInfo = (
12
+ overrides: Partial<{
13
+ subject: string;
14
+ issuer: string;
15
+ issuerOrg: string | undefined;
16
+ validFrom: Date;
17
+ validTo: Date;
18
+ }> = {}
19
+ ): CertificateInfo => {
20
+ const validFrom =
21
+ overrides.validFrom ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
22
+ const validTo =
23
+ overrides.validTo ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
24
+
25
+ // Check if issuerOrg was explicitly set (even to undefined)
26
+ const hasIssuerOrg = "issuerOrg" in overrides;
27
+ const issuerOrg = hasIssuerOrg ? overrides.issuerOrg : "DigiCert Inc";
28
+
29
+ return {
30
+ subject: { CN: overrides.subject ?? "example.com" },
31
+ issuer: {
32
+ CN: overrides.issuer ?? "DigiCert",
33
+ O: issuerOrg,
34
+ },
35
+ valid_from: validFrom.toISOString(),
36
+ valid_to: validTo.toISOString(),
37
+ };
38
+ };
39
+
40
+ // Helper to create mock TLS client
41
+ const createMockClient = (
42
+ config: {
43
+ authorized?: boolean;
44
+ cert?: CertificateInfo;
45
+ protocol?: string;
46
+ cipher?: string;
47
+ error?: Error;
48
+ } = {}
49
+ ): TlsClient => ({
50
+ connect: mock(() =>
51
+ config.error
52
+ ? Promise.reject(config.error)
53
+ : Promise.resolve({
54
+ authorized: config.authorized ?? true,
55
+ getPeerCertificate: () => config.cert ?? createCertInfo(),
56
+ getProtocol: () => config.protocol ?? "TLSv1.3",
57
+ getCipher: () => (config.cipher ? { name: config.cipher } : null),
58
+ end: mock(() => {}),
59
+ } as TlsConnection)
60
+ ),
61
+ });
62
+
63
+ describe("execute", () => {
64
+ it("should return healthy for valid certificate", async () => {
65
+ const strategy = new TlsHealthCheckStrategy(createMockClient());
66
+
67
+ const result = await strategy.execute({
68
+ host: "example.com",
69
+ port: 443,
70
+ timeout: 5000,
71
+ minDaysUntilExpiry: 7,
72
+ rejectUnauthorized: true,
73
+ });
74
+
75
+ expect(result.status).toBe("healthy");
76
+ expect(result.metadata?.isValid).toBe(true);
77
+ expect(result.metadata?.daysUntilExpiry).toBeGreaterThan(0);
78
+ });
79
+
80
+ it("should return unhealthy for unauthorized certificate", async () => {
81
+ const strategy = new TlsHealthCheckStrategy(
82
+ createMockClient({ authorized: false })
83
+ );
84
+
85
+ const result = await strategy.execute({
86
+ host: "example.com",
87
+ port: 443,
88
+ timeout: 5000,
89
+ minDaysUntilExpiry: 7,
90
+ rejectUnauthorized: true,
91
+ });
92
+
93
+ expect(result.status).toBe("unhealthy");
94
+ expect(result.message).toContain("not valid");
95
+ });
96
+
97
+ it("should return unhealthy when certificate expires soon", async () => {
98
+ const expiringCert = createCertInfo({
99
+ validTo: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days
100
+ });
101
+
102
+ const strategy = new TlsHealthCheckStrategy(
103
+ createMockClient({ cert: expiringCert })
104
+ );
105
+
106
+ const result = await strategy.execute({
107
+ host: "example.com",
108
+ port: 443,
109
+ timeout: 5000,
110
+ minDaysUntilExpiry: 14,
111
+ rejectUnauthorized: true,
112
+ });
113
+
114
+ expect(result.status).toBe("unhealthy");
115
+ expect(result.message).toContain("expires in");
116
+ });
117
+
118
+ it("should return unhealthy for connection error", async () => {
119
+ const strategy = new TlsHealthCheckStrategy(
120
+ createMockClient({ error: new Error("Connection refused") })
121
+ );
122
+
123
+ const result = await strategy.execute({
124
+ host: "example.com",
125
+ port: 443,
126
+ timeout: 5000,
127
+ minDaysUntilExpiry: 7,
128
+ rejectUnauthorized: true,
129
+ });
130
+
131
+ expect(result.status).toBe("unhealthy");
132
+ expect(result.message).toContain("Connection refused");
133
+ });
134
+
135
+ it("should pass daysUntilExpiry assertion", async () => {
136
+ const strategy = new TlsHealthCheckStrategy(createMockClient());
137
+
138
+ const result = await strategy.execute({
139
+ host: "example.com",
140
+ port: 443,
141
+ timeout: 5000,
142
+ minDaysUntilExpiry: 7,
143
+ rejectUnauthorized: true,
144
+ assertions: [
145
+ {
146
+ field: "daysUntilExpiry",
147
+ operator: "greaterThanOrEqual",
148
+ value: 7,
149
+ },
150
+ ],
151
+ });
152
+
153
+ expect(result.status).toBe("healthy");
154
+ });
155
+
156
+ it("should pass issuer assertion", async () => {
157
+ const strategy = new TlsHealthCheckStrategy(createMockClient());
158
+
159
+ const result = await strategy.execute({
160
+ host: "example.com",
161
+ port: 443,
162
+ timeout: 5000,
163
+ minDaysUntilExpiry: 7,
164
+ rejectUnauthorized: true,
165
+ assertions: [
166
+ { field: "issuer", operator: "contains", value: "DigiCert" },
167
+ ],
168
+ });
169
+
170
+ expect(result.status).toBe("healthy");
171
+ });
172
+
173
+ it("should fail isValid assertion when certificate is invalid", async () => {
174
+ const strategy = new TlsHealthCheckStrategy(
175
+ createMockClient({ authorized: false })
176
+ );
177
+
178
+ const result = await strategy.execute({
179
+ host: "example.com",
180
+ port: 443,
181
+ timeout: 5000,
182
+ minDaysUntilExpiry: 7,
183
+ rejectUnauthorized: false, // Don't reject to test assertion
184
+ assertions: [{ field: "isValid", operator: "isTrue" }],
185
+ });
186
+
187
+ expect(result.status).toBe("unhealthy");
188
+ expect(result.message).toContain("Assertion failed");
189
+ });
190
+
191
+ it("should detect self-signed certificates", async () => {
192
+ const selfSignedCert = createCertInfo({
193
+ subject: "localhost",
194
+ issuer: "localhost",
195
+ issuerOrg: undefined,
196
+ });
197
+
198
+ const strategy = new TlsHealthCheckStrategy(
199
+ createMockClient({ cert: selfSignedCert, authorized: false })
200
+ );
201
+
202
+ const result = await strategy.execute({
203
+ host: "localhost",
204
+ port: 443,
205
+ timeout: 5000,
206
+ minDaysUntilExpiry: 0,
207
+ rejectUnauthorized: false,
208
+ });
209
+
210
+ expect(result.metadata?.isSelfSigned).toBe(true);
211
+ });
212
+ });
213
+
214
+ describe("aggregateResult", () => {
215
+ it("should calculate averages correctly", () => {
216
+ const strategy = new TlsHealthCheckStrategy();
217
+ const runs = [
218
+ {
219
+ id: "1",
220
+ status: "healthy" as const,
221
+ latencyMs: 100,
222
+ checkId: "c1",
223
+ timestamp: new Date(),
224
+ metadata: {
225
+ connected: true,
226
+ isValid: true,
227
+ isSelfSigned: false,
228
+ issuer: "DigiCert",
229
+ subject: "example.com",
230
+ validFrom: "2024-01-01",
231
+ validTo: "2025-01-01",
232
+ daysUntilExpiry: 30,
233
+ },
234
+ },
235
+ {
236
+ id: "2",
237
+ status: "healthy" as const,
238
+ latencyMs: 150,
239
+ checkId: "c1",
240
+ timestamp: new Date(),
241
+ metadata: {
242
+ connected: true,
243
+ isValid: true,
244
+ isSelfSigned: false,
245
+ issuer: "DigiCert",
246
+ subject: "example.com",
247
+ validFrom: "2024-01-01",
248
+ validTo: "2025-01-01",
249
+ daysUntilExpiry: 20,
250
+ },
251
+ },
252
+ ];
253
+
254
+ const aggregated = strategy.aggregateResult(runs);
255
+
256
+ expect(aggregated.avgDaysUntilExpiry).toBe(25);
257
+ expect(aggregated.minDaysUntilExpiry).toBe(20);
258
+ expect(aggregated.invalidCount).toBe(0);
259
+ expect(aggregated.errorCount).toBe(0);
260
+ });
261
+
262
+ it("should count invalid and errors", () => {
263
+ const strategy = new TlsHealthCheckStrategy();
264
+ const runs = [
265
+ {
266
+ id: "1",
267
+ status: "unhealthy" as const,
268
+ latencyMs: 100,
269
+ checkId: "c1",
270
+ timestamp: new Date(),
271
+ metadata: {
272
+ connected: true,
273
+ isValid: false,
274
+ isSelfSigned: false,
275
+ issuer: "",
276
+ subject: "",
277
+ validFrom: "",
278
+ validTo: "",
279
+ daysUntilExpiry: 0,
280
+ },
281
+ },
282
+ {
283
+ id: "2",
284
+ status: "unhealthy" as const,
285
+ latencyMs: 0,
286
+ checkId: "c1",
287
+ timestamp: new Date(),
288
+ metadata: {
289
+ connected: false,
290
+ isValid: false,
291
+ isSelfSigned: false,
292
+ issuer: "",
293
+ subject: "",
294
+ validFrom: "",
295
+ validTo: "",
296
+ daysUntilExpiry: 0,
297
+ error: "Connection refused",
298
+ },
299
+ },
300
+ ];
301
+
302
+ const aggregated = strategy.aggregateResult(runs);
303
+
304
+ expect(aggregated.invalidCount).toBe(1);
305
+ expect(aggregated.errorCount).toBe(1);
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,392 @@
1
+ import * as tls from "node:tls";
2
+ import {
3
+ HealthCheckStrategy,
4
+ HealthCheckResult,
5
+ HealthCheckRunForAggregation,
6
+ Versioned,
7
+ z,
8
+ numericField,
9
+ stringField,
10
+ booleanField,
11
+ evaluateAssertions,
12
+ } from "@checkstack/backend-api";
13
+ import {
14
+ healthResultBoolean,
15
+ healthResultNumber,
16
+ healthResultString,
17
+ } from "@checkstack/healthcheck-common";
18
+
19
+ // ============================================================================
20
+ // SCHEMAS
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Assertion schema for TLS health checks using shared factories.
25
+ */
26
+ const tlsAssertionSchema = z.discriminatedUnion("field", [
27
+ numericField("daysUntilExpiry", { min: 0 }),
28
+ stringField("issuer"),
29
+ stringField("subject"),
30
+ booleanField("isValid"),
31
+ booleanField("isSelfSigned"),
32
+ ]);
33
+
34
+ export type TlsAssertion = z.infer<typeof tlsAssertionSchema>;
35
+
36
+ /**
37
+ * Configuration schema for TLS health checks.
38
+ */
39
+ export const tlsConfigSchema = z.object({
40
+ host: z.string().describe("Hostname to connect to"),
41
+ port: z.number().int().min(1).max(65_535).default(443).describe("TLS port"),
42
+ servername: z
43
+ .string()
44
+ .optional()
45
+ .describe("SNI hostname (defaults to host if not specified)"),
46
+ timeout: z
47
+ .number()
48
+ .min(100)
49
+ .default(10_000)
50
+ .describe("Connection timeout in milliseconds"),
51
+ minDaysUntilExpiry: z
52
+ .number()
53
+ .min(0)
54
+ .default(14)
55
+ .describe("Minimum days until certificate expiry for healthy status"),
56
+ rejectUnauthorized: z
57
+ .boolean()
58
+ .default(true)
59
+ .describe("Reject invalid/self-signed certificates"),
60
+ assertions: z
61
+ .array(tlsAssertionSchema)
62
+ .optional()
63
+ .describe("Validation conditions"),
64
+ });
65
+
66
+ export type TlsConfig = z.infer<typeof tlsConfigSchema>;
67
+
68
+ /**
69
+ * Per-run result metadata.
70
+ */
71
+ const tlsResultSchema = z.object({
72
+ connected: healthResultBoolean({
73
+ "x-chart-type": "boolean",
74
+ "x-chart-label": "Connected",
75
+ }),
76
+ isValid: healthResultBoolean({
77
+ "x-chart-type": "boolean",
78
+ "x-chart-label": "Certificate Valid",
79
+ }),
80
+ isSelfSigned: healthResultBoolean({
81
+ "x-chart-type": "boolean",
82
+ "x-chart-label": "Self-Signed",
83
+ }),
84
+ issuer: healthResultString({
85
+ "x-chart-type": "text",
86
+ "x-chart-label": "Issuer",
87
+ }),
88
+ subject: healthResultString({
89
+ "x-chart-type": "text",
90
+ "x-chart-label": "Subject",
91
+ }),
92
+ validFrom: healthResultString({
93
+ "x-chart-type": "text",
94
+ "x-chart-label": "Valid From",
95
+ }),
96
+ validTo: healthResultString({
97
+ "x-chart-type": "text",
98
+ "x-chart-label": "Valid To",
99
+ }),
100
+ daysUntilExpiry: healthResultNumber({
101
+ "x-chart-type": "counter",
102
+ "x-chart-label": "Days Until Expiry",
103
+ "x-chart-unit": "days",
104
+ }),
105
+ protocol: healthResultString({
106
+ "x-chart-type": "text",
107
+ "x-chart-label": "Protocol",
108
+ }).optional(),
109
+ cipher: healthResultString({
110
+ "x-chart-type": "text",
111
+ "x-chart-label": "Cipher",
112
+ }).optional(),
113
+ failedAssertion: tlsAssertionSchema.optional(),
114
+ error: healthResultString({
115
+ "x-chart-type": "status",
116
+ "x-chart-label": "Error",
117
+ }).optional(),
118
+ });
119
+
120
+ export type TlsResult = z.infer<typeof tlsResultSchema>;
121
+
122
+ /**
123
+ * Aggregated metadata for buckets.
124
+ */
125
+ const tlsAggregatedSchema = z.object({
126
+ avgDaysUntilExpiry: healthResultNumber({
127
+ "x-chart-type": "line",
128
+ "x-chart-label": "Avg Days Until Expiry",
129
+ "x-chart-unit": "days",
130
+ }),
131
+ minDaysUntilExpiry: healthResultNumber({
132
+ "x-chart-type": "line",
133
+ "x-chart-label": "Min Days Until Expiry",
134
+ "x-chart-unit": "days",
135
+ }),
136
+ invalidCount: healthResultNumber({
137
+ "x-chart-type": "counter",
138
+ "x-chart-label": "Invalid Certificates",
139
+ }),
140
+ errorCount: healthResultNumber({
141
+ "x-chart-type": "counter",
142
+ "x-chart-label": "Errors",
143
+ }),
144
+ });
145
+
146
+ export type TlsAggregatedResult = z.infer<typeof tlsAggregatedSchema>;
147
+
148
+ // ============================================================================
149
+ // TLS CLIENT INTERFACE (for testability)
150
+ // ============================================================================
151
+
152
+ export interface CertificateInfo {
153
+ subject: { CN?: string };
154
+ issuer: { CN?: string; O?: string };
155
+ valid_from: string;
156
+ valid_to: string;
157
+ }
158
+
159
+ export interface TlsConnection {
160
+ authorized: boolean;
161
+ getPeerCertificate(): CertificateInfo;
162
+ getProtocol(): string | null;
163
+ getCipher(): { name: string } | null;
164
+ end(): void;
165
+ }
166
+
167
+ export interface TlsClient {
168
+ connect(options: {
169
+ host: string;
170
+ port: number;
171
+ servername: string;
172
+ rejectUnauthorized: boolean;
173
+ timeout: number;
174
+ }): Promise<TlsConnection>;
175
+ }
176
+
177
+ // Default client using Node.js tls module
178
+ const defaultTlsClient: TlsClient = {
179
+ connect(options): Promise<TlsConnection> {
180
+ return new Promise((resolve, reject) => {
181
+ const socket = tls.connect(
182
+ {
183
+ host: options.host,
184
+ port: options.port,
185
+ servername: options.servername,
186
+ rejectUnauthorized: options.rejectUnauthorized,
187
+ timeout: options.timeout,
188
+ },
189
+ () => {
190
+ resolve({
191
+ authorized: socket.authorized,
192
+ getPeerCertificate: () =>
193
+ socket.getPeerCertificate() as unknown as CertificateInfo,
194
+ getProtocol: () => socket.getProtocol(),
195
+ getCipher: () => socket.getCipher(),
196
+ end: () => socket.end(),
197
+ });
198
+ }
199
+ );
200
+
201
+ socket.on("error", reject);
202
+ socket.on("timeout", () => {
203
+ socket.destroy();
204
+ reject(new Error("Connection timeout"));
205
+ });
206
+ });
207
+ },
208
+ };
209
+
210
+ // ============================================================================
211
+ // STRATEGY
212
+ // ============================================================================
213
+
214
+ export class TlsHealthCheckStrategy
215
+ implements HealthCheckStrategy<TlsConfig, TlsResult, TlsAggregatedResult>
216
+ {
217
+ id = "tls";
218
+ displayName = "TLS/SSL Health Check";
219
+ description = "SSL/TLS certificate validation and expiry monitoring";
220
+
221
+ private tlsClient: TlsClient;
222
+
223
+ constructor(tlsClient: TlsClient = defaultTlsClient) {
224
+ this.tlsClient = tlsClient;
225
+ }
226
+
227
+ config: Versioned<TlsConfig> = new Versioned({
228
+ version: 1,
229
+ schema: tlsConfigSchema,
230
+ });
231
+
232
+ result: Versioned<TlsResult> = new Versioned({
233
+ version: 1,
234
+ schema: tlsResultSchema,
235
+ });
236
+
237
+ aggregatedResult: Versioned<TlsAggregatedResult> = new Versioned({
238
+ version: 1,
239
+ schema: tlsAggregatedSchema,
240
+ });
241
+
242
+ aggregateResult(
243
+ runs: HealthCheckRunForAggregation<TlsResult>[]
244
+ ): TlsAggregatedResult {
245
+ let totalDaysUntilExpiry = 0;
246
+ let minDaysUntilExpiry = Number.POSITIVE_INFINITY;
247
+ let invalidCount = 0;
248
+ let errorCount = 0;
249
+ let validRuns = 0;
250
+
251
+ for (const run of runs) {
252
+ if (run.metadata?.error) {
253
+ errorCount++;
254
+ continue;
255
+ }
256
+ if (run.metadata && !run.metadata.isValid) {
257
+ invalidCount++;
258
+ }
259
+ if (run.metadata) {
260
+ totalDaysUntilExpiry += run.metadata.daysUntilExpiry;
261
+ if (run.metadata.daysUntilExpiry < minDaysUntilExpiry) {
262
+ minDaysUntilExpiry = run.metadata.daysUntilExpiry;
263
+ }
264
+ validRuns++;
265
+ }
266
+ }
267
+
268
+ return {
269
+ avgDaysUntilExpiry: validRuns > 0 ? totalDaysUntilExpiry / validRuns : 0,
270
+ minDaysUntilExpiry:
271
+ minDaysUntilExpiry === Number.POSITIVE_INFINITY
272
+ ? 0
273
+ : minDaysUntilExpiry,
274
+ invalidCount,
275
+ errorCount,
276
+ };
277
+ }
278
+
279
+ async execute(config: TlsConfig): Promise<HealthCheckResult<TlsResult>> {
280
+ const validatedConfig = this.config.validate(config);
281
+ const start = performance.now();
282
+
283
+ try {
284
+ const connection = await this.tlsClient.connect({
285
+ host: validatedConfig.host,
286
+ port: validatedConfig.port,
287
+ servername: validatedConfig.servername ?? validatedConfig.host,
288
+ rejectUnauthorized: validatedConfig.rejectUnauthorized,
289
+ timeout: validatedConfig.timeout,
290
+ });
291
+
292
+ const cert = connection.getPeerCertificate();
293
+ const protocol = connection.getProtocol();
294
+ const cipher = connection.getCipher();
295
+
296
+ connection.end();
297
+
298
+ const end = performance.now();
299
+ const latencyMs = Math.round(end - start);
300
+
301
+ // Calculate days until expiry
302
+ const validTo = new Date(cert.valid_to);
303
+ const now = new Date();
304
+ const daysUntilExpiry = Math.floor(
305
+ (validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
306
+ );
307
+
308
+ // Determine if self-signed
309
+ const isSelfSigned =
310
+ cert.issuer.CN === cert.subject.CN && cert.issuer.O === undefined;
311
+
312
+ const result: Omit<TlsResult, "failedAssertion" | "error"> = {
313
+ connected: true,
314
+ isValid: connection.authorized,
315
+ isSelfSigned,
316
+ issuer: cert.issuer.CN ?? cert.issuer.O ?? "Unknown",
317
+ subject: cert.subject.CN ?? "Unknown",
318
+ validFrom: cert.valid_from,
319
+ validTo: cert.valid_to,
320
+ daysUntilExpiry,
321
+ protocol: protocol ?? undefined,
322
+ cipher: cipher?.name,
323
+ };
324
+
325
+ // Evaluate assertions using shared utility
326
+ const failedAssertion = evaluateAssertions(validatedConfig.assertions, {
327
+ daysUntilExpiry,
328
+ issuer: result.issuer,
329
+ subject: result.subject,
330
+ isValid: result.isValid,
331
+ isSelfSigned,
332
+ });
333
+
334
+ if (failedAssertion) {
335
+ return {
336
+ status: "unhealthy",
337
+ latencyMs,
338
+ message: `Assertion failed: ${failedAssertion.field} ${
339
+ failedAssertion.operator
340
+ }${"value" in failedAssertion ? ` ${failedAssertion.value}` : ""}`,
341
+ metadata: { ...result, failedAssertion },
342
+ };
343
+ }
344
+
345
+ // Check minimum days until expiry
346
+ if (daysUntilExpiry < validatedConfig.minDaysUntilExpiry) {
347
+ return {
348
+ status: "unhealthy",
349
+ latencyMs,
350
+ message: `Certificate expires in ${daysUntilExpiry} days (minimum: ${validatedConfig.minDaysUntilExpiry})`,
351
+ metadata: result,
352
+ };
353
+ }
354
+
355
+ // Check certificate validity
356
+ if (!connection.authorized && validatedConfig.rejectUnauthorized) {
357
+ return {
358
+ status: "unhealthy",
359
+ latencyMs,
360
+ message: "Certificate is not valid or not trusted",
361
+ metadata: result,
362
+ };
363
+ }
364
+
365
+ return {
366
+ status: "healthy",
367
+ latencyMs,
368
+ message: `Certificate valid for ${daysUntilExpiry} days (${result.subject} issued by ${result.issuer})`,
369
+ metadata: result,
370
+ };
371
+ } catch (error: unknown) {
372
+ const end = performance.now();
373
+ const isError = error instanceof Error;
374
+ return {
375
+ status: "unhealthy",
376
+ latencyMs: Math.round(end - start),
377
+ message: isError ? error.message : "TLS connection failed",
378
+ metadata: {
379
+ connected: false,
380
+ isValid: false,
381
+ isSelfSigned: false,
382
+ issuer: "",
383
+ subject: "",
384
+ validFrom: "",
385
+ validTo: "",
386
+ daysUntilExpiry: 0,
387
+ error: isError ? error.name : "UnknownError",
388
+ },
389
+ };
390
+ }
391
+ }
392
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }