@checkstack/healthcheck-tls-backend 0.1.13 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @checkstack/healthcheck-tls-backend
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
8
+
9
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
10
+
11
+ **Key changes:**
12
+
13
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
14
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
15
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
16
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
17
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
18
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
19
+
20
+ **Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [3dd1914]
25
+ - @checkstack/backend-api@0.7.0
26
+
27
+ ## 0.1.14
28
+
29
+ ### Patch Changes
30
+
31
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
32
+
33
+ ### Breaking Changes (Internal)
34
+
35
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
36
+
37
+ ### New Features
38
+
39
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
40
+ - `mergeCounter()` - track occurrences
41
+ - `mergeAverage()` - track sum/count, compute avg
42
+ - `mergeRate()` - track success/total, compute %
43
+ - `mergeMinMax()` - track min/max values
44
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
45
+
46
+ ### Improvements
47
+
48
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
49
+ - Prepares for real-time hourly aggregation without batch accumulation
50
+
51
+ - Updated dependencies [f676e11]
52
+ - Updated dependencies [48c2080]
53
+ - @checkstack/common@0.6.2
54
+ - @checkstack/backend-api@0.6.0
55
+ - @checkstack/healthcheck-common@0.8.2
56
+
3
57
  ## 0.1.13
4
58
 
5
59
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-tls-backend",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,14 +9,14 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.1",
13
- "@checkstack/common": "0.6.0",
14
- "@checkstack/healthcheck-common": "0.7.0"
12
+ "@checkstack/backend-api": "0.5.2",
13
+ "@checkstack/common": "0.6.1",
14
+ "@checkstack/healthcheck-common": "0.8.1"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/bun": "^1.0.0",
18
18
  "typescript": "^5.0.0",
19
- "@checkstack/tsconfig": "0.0.2",
20
- "@checkstack/scripts": "0.1.0"
19
+ "@checkstack/tsconfig": "0.0.3",
20
+ "@checkstack/scripts": "0.1.1"
21
21
  }
22
22
  }
@@ -76,7 +76,7 @@ describe("CertificateCollector", () => {
76
76
  });
77
77
  });
78
78
 
79
- describe("aggregateResult", () => {
79
+ describe("mergeResult", () => {
80
80
  it("should calculate average days remaining", () => {
81
81
  const collector = new CertificateCollector();
82
82
  const runs = [
@@ -112,10 +112,11 @@ describe("CertificateCollector", () => {
112
112
  },
113
113
  ];
114
114
 
115
- const aggregated = collector.aggregateResult(runs);
115
+ let aggregated = collector.mergeResult(undefined, runs[0]);
116
+ aggregated = collector.mergeResult(aggregated, runs[1]);
116
117
 
117
- expect(aggregated.avgDaysRemaining).toBe(45);
118
- expect(aggregated.validRate).toBe(100);
118
+ expect(aggregated.avgDaysRemaining.avg).toBe(45);
119
+ expect(aggregated.validRate.rate).toBe(100);
119
120
  });
120
121
 
121
122
  it("should calculate valid rate correctly", () => {
@@ -153,9 +154,10 @@ describe("CertificateCollector", () => {
153
154
  },
154
155
  ];
155
156
 
156
- const aggregated = collector.aggregateResult(runs);
157
+ let aggregated = collector.mergeResult(undefined, runs[0]);
158
+ aggregated = collector.mergeResult(aggregated, runs[1]);
157
159
 
158
- expect(aggregated.validRate).toBe(50);
160
+ expect(aggregated.validRate.rate).toBe(50);
159
161
  });
160
162
  });
161
163
 
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ mergeRate,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -58,21 +64,23 @@ const certificateResultSchema = healthResultSchema({
58
64
 
59
65
  export type CertificateResult = z.infer<typeof certificateResultSchema>;
60
66
 
61
- const certificateAggregatedSchema = healthResultSchema({
62
- avgDaysRemaining: healthResultNumber({
67
+ // Aggregated result fields definition
68
+ const certificateAggregatedFields = {
69
+ avgDaysRemaining: aggregatedAverage({
63
70
  "x-chart-type": "gauge",
64
71
  "x-chart-label": "Avg Days Remaining",
65
72
  "x-chart-unit": "days",
66
73
  }),
67
- validRate: healthResultNumber({
74
+ validRate: aggregatedRate({
68
75
  "x-chart-type": "gauge",
69
76
  "x-chart-label": "Valid Rate",
70
77
  "x-chart-unit": "%",
71
78
  }),
72
- });
79
+ };
73
80
 
74
- export type CertificateAggregatedResult = z.infer<
75
- typeof certificateAggregatedSchema
81
+ // Type inferred from field definitions
82
+ export type CertificateAggregatedResult = InferAggregatedResult<
83
+ typeof certificateAggregatedFields
76
84
  >;
77
85
 
78
86
  // ============================================================================
@@ -83,15 +91,12 @@ export type CertificateAggregatedResult = z.infer<
83
91
  * Built-in TLS certificate collector.
84
92
  * Returns certificate information from the TLS connection.
85
93
  */
86
- export class CertificateCollector
87
- implements
88
- CollectorStrategy<
89
- TlsTransportClient,
90
- CertificateConfig,
91
- CertificateResult,
92
- CertificateAggregatedResult
93
- >
94
- {
94
+ export class CertificateCollector implements CollectorStrategy<
95
+ TlsTransportClient,
96
+ CertificateConfig,
97
+ CertificateResult,
98
+ CertificateAggregatedResult
99
+ > {
95
100
  id = "certificate";
96
101
  displayName = "TLS Certificate";
97
102
  description = "Check TLS certificate validity and expiration";
@@ -102,9 +107,9 @@ export class CertificateCollector
102
107
 
103
108
  config = new Versioned({ version: 1, schema: certificateConfigSchema });
104
109
  result = new Versioned({ version: 1, schema: certificateResultSchema });
105
- aggregatedResult = new Versioned({
110
+ aggregatedResult = new VersionedAggregated({
106
111
  version: 1,
107
- schema: certificateAggregatedSchema,
112
+ fields: certificateAggregatedFields,
108
113
  });
109
114
 
110
115
  async execute({
@@ -142,30 +147,18 @@ export class CertificateCollector
142
147
  };
143
148
  }
144
149
 
145
- aggregateResult(
146
- runs: HealthCheckRunForAggregation<CertificateResult>[]
150
+ mergeResult(
151
+ existing: CertificateAggregatedResult | undefined,
152
+ run: HealthCheckRunForAggregation<CertificateResult>,
147
153
  ): CertificateAggregatedResult {
148
- const daysRemaining = runs
149
- .map((r) => r.metadata?.daysRemaining)
150
- .filter((v): v is number => typeof v === "number");
151
-
152
- const validResults = runs
153
- .map((r) => r.metadata?.valid)
154
- .filter((v): v is boolean => typeof v === "boolean");
155
-
156
- const validCount = validResults.filter(Boolean).length;
154
+ const metadata = run.metadata;
157
155
 
158
156
  return {
159
- avgDaysRemaining:
160
- daysRemaining.length > 0
161
- ? Math.round(
162
- daysRemaining.reduce((a, b) => a + b, 0) / daysRemaining.length
163
- )
164
- : 0,
165
- validRate:
166
- validResults.length > 0
167
- ? Math.round((validCount / validResults.length) * 100)
168
- : 0,
157
+ avgDaysRemaining: mergeAverage(
158
+ existing?.avgDaysRemaining,
159
+ metadata?.daysRemaining,
160
+ ),
161
+ validRate: mergeRate(existing?.validRate, metadata?.valid),
169
162
  };
170
163
  }
171
164
  }
@@ -164,7 +164,7 @@ describe("TlsHealthCheckStrategy", () => {
164
164
  });
165
165
  });
166
166
 
167
- describe("aggregateResult", () => {
167
+ describe("mergeResult", () => {
168
168
  it("should calculate averages correctly", () => {
169
169
  const strategy = new TlsHealthCheckStrategy();
170
170
  const runs = [
@@ -204,12 +204,13 @@ describe("TlsHealthCheckStrategy", () => {
204
204
  },
205
205
  ];
206
206
 
207
- const aggregated = strategy.aggregateResult(runs);
207
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
208
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
208
209
 
209
- expect(aggregated.avgDaysUntilExpiry).toBe(25);
210
- expect(aggregated.minDaysUntilExpiry).toBe(20);
211
- expect(aggregated.invalidCount).toBe(0);
212
- expect(aggregated.errorCount).toBe(0);
210
+ expect(aggregated.avgDaysUntilExpiry.avg).toBe(25);
211
+ expect(aggregated.minDaysUntilExpiry.min).toBe(20);
212
+ expect(aggregated.invalidCount.count).toBe(0);
213
+ expect(aggregated.errorCount.count).toBe(0);
213
214
  });
214
215
 
215
216
  it("should count invalid and errors", () => {
@@ -252,10 +253,11 @@ describe("TlsHealthCheckStrategy", () => {
252
253
  },
253
254
  ];
254
255
 
255
- const aggregated = strategy.aggregateResult(runs);
256
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
257
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
256
258
 
257
- expect(aggregated.invalidCount).toBe(2);
258
- expect(aggregated.errorCount).toBe(1);
259
+ expect(aggregated.invalidCount.count).toBe(2);
260
+ expect(aggregated.errorCount.count).toBe(1);
259
261
  });
260
262
  });
261
263
  });
package/src/strategy.ts CHANGED
@@ -3,8 +3,16 @@ import {
3
3
  HealthCheckStrategy,
4
4
  HealthCheckRunForAggregation,
5
5
  Versioned,
6
+ VersionedAggregated,
7
+ aggregatedAverage,
8
+ aggregatedMinMax,
9
+ aggregatedCounter,
10
+ mergeAverage,
11
+ mergeCounter,
12
+ mergeMinMax,
6
13
  z,
7
14
  type ConnectedClient,
15
+ type InferAggregatedResult,
8
16
  } from "@checkstack/backend-api";
9
17
  import {
10
18
  healthResultBoolean,
@@ -80,31 +88,29 @@ const tlsResultSchema = healthResultSchema({
80
88
 
81
89
  type TlsResult = z.infer<typeof tlsResultSchema>;
82
90
 
83
- /**
84
- * Aggregated metadata for buckets.
85
- */
86
- const tlsAggregatedSchema = healthResultSchema({
87
- avgDaysUntilExpiry: healthResultNumber({
91
+ /** Aggregated field definitions for bucket merging */
92
+ const tlsAggregatedFields = {
93
+ avgDaysUntilExpiry: aggregatedAverage({
88
94
  "x-chart-type": "line",
89
95
  "x-chart-label": "Avg Days Until Expiry",
90
96
  "x-chart-unit": "days",
91
97
  }),
92
- minDaysUntilExpiry: healthResultNumber({
98
+ minDaysUntilExpiry: aggregatedMinMax({
93
99
  "x-chart-type": "line",
94
100
  "x-chart-label": "Min Days Until Expiry",
95
101
  "x-chart-unit": "days",
96
102
  }),
97
- invalidCount: healthResultNumber({
103
+ invalidCount: aggregatedCounter({
98
104
  "x-chart-type": "counter",
99
105
  "x-chart-label": "Invalid Certificates",
100
106
  }),
101
- errorCount: healthResultNumber({
107
+ errorCount: aggregatedCounter({
102
108
  "x-chart-type": "counter",
103
109
  "x-chart-label": "Errors",
104
110
  }),
105
- });
111
+ };
106
112
 
107
- type TlsAggregatedResult = z.infer<typeof tlsAggregatedSchema>;
113
+ type TlsAggregatedResult = InferAggregatedResult<typeof tlsAggregatedFields>;
108
114
 
109
115
  // ============================================================================
110
116
  // TLS CLIENT INTERFACE (for testability)
@@ -156,7 +162,7 @@ const defaultTlsClient: TlsClient = {
156
162
  getCipher: () => socket.getCipher(),
157
163
  end: () => socket.end(),
158
164
  });
159
- }
165
+ },
160
166
  );
161
167
 
162
168
  socket.on("error", reject);
@@ -172,15 +178,12 @@ const defaultTlsClient: TlsClient = {
172
178
  // STRATEGY
173
179
  // ============================================================================
174
180
 
175
- export class TlsHealthCheckStrategy
176
- implements
177
- HealthCheckStrategy<
178
- TlsConfig,
179
- TlsTransportClient,
180
- TlsResult,
181
- TlsAggregatedResult
182
- >
183
- {
181
+ export class TlsHealthCheckStrategy implements HealthCheckStrategy<
182
+ TlsConfig,
183
+ TlsTransportClient,
184
+ TlsResult,
185
+ typeof tlsAggregatedFields
186
+ > {
184
187
  id = "tls";
185
188
  displayName = "TLS/SSL Health Check";
186
189
  description = "SSL/TLS certificate validation and expiry monitoring";
@@ -217,55 +220,38 @@ export class TlsHealthCheckStrategy
217
220
  ],
218
221
  });
219
222
 
220
- aggregatedResult: Versioned<TlsAggregatedResult> = new Versioned({
223
+ aggregatedResult = new VersionedAggregated({
221
224
  version: 1,
222
- schema: tlsAggregatedSchema,
225
+ fields: tlsAggregatedFields,
223
226
  });
224
227
 
225
- aggregateResult(
226
- runs: HealthCheckRunForAggregation<TlsResult>[]
228
+ mergeResult(
229
+ existing: TlsAggregatedResult | undefined,
230
+ run: HealthCheckRunForAggregation<TlsResult>,
227
231
  ): TlsAggregatedResult {
228
- const validRuns = runs.filter((r) => r.metadata);
232
+ const metadata = run.metadata;
229
233
 
230
- if (validRuns.length === 0) {
231
- return {
232
- avgDaysUntilExpiry: 0,
233
- minDaysUntilExpiry: 0,
234
- invalidCount: 0,
235
- errorCount: 0,
236
- };
237
- }
238
-
239
- const daysValues = validRuns
240
- .map((r) => r.metadata?.daysUntilExpiry)
241
- .filter((d): d is number => typeof d === "number");
242
-
243
- const avgDaysUntilExpiry =
244
- daysValues.length > 0
245
- ? Math.round(daysValues.reduce((a, b) => a + b, 0) / daysValues.length)
246
- : 0;
234
+ const avgDaysUntilExpiry = mergeAverage(
235
+ existing?.avgDaysUntilExpiry,
236
+ metadata?.daysUntilExpiry,
237
+ );
247
238
 
248
- const minDaysUntilExpiry =
249
- daysValues.length > 0 ? Math.min(...daysValues) : 0;
239
+ const minDaysUntilExpiry = mergeMinMax(
240
+ existing?.minDaysUntilExpiry,
241
+ metadata?.daysUntilExpiry,
242
+ );
250
243
 
251
- const invalidCount = validRuns.filter(
252
- (r) => r.metadata?.isValid === false
253
- ).length;
244
+ const isInvalid = metadata?.isValid === false;
245
+ const invalidCount = mergeCounter(existing?.invalidCount, isInvalid);
254
246
 
255
- const errorCount = validRuns.filter(
256
- (r) => r.metadata?.error !== undefined
257
- ).length;
247
+ const hasError = metadata?.error !== undefined;
248
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
258
249
 
259
- return {
260
- avgDaysUntilExpiry,
261
- minDaysUntilExpiry,
262
- invalidCount,
263
- errorCount,
264
- };
250
+ return { avgDaysUntilExpiry, minDaysUntilExpiry, invalidCount, errorCount };
265
251
  }
266
252
 
267
253
  async createClient(
268
- config: TlsConfig
254
+ config: TlsConfig,
269
255
  ): Promise<ConnectedClient<TlsTransportClient>> {
270
256
  const validatedConfig = this.config.validate(config);
271
257
 
@@ -280,7 +266,7 @@ export class TlsHealthCheckStrategy
280
266
  const cert = connection.getPeerCertificate();
281
267
  const validTo = new Date(cert.valid_to);
282
268
  const daysUntilExpiry = Math.floor(
283
- (validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
269
+ (validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24),
284
270
  );
285
271
 
286
272
  const certInfo: TlsCertificateInfo = {