@checkstack/healthcheck-tls-backend 0.1.13 → 0.1.14

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,35 @@
1
1
  # @checkstack/healthcheck-tls-backend
2
2
 
3
+ ## 0.1.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
8
+
9
+ ### Breaking Changes (Internal)
10
+
11
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
12
+
13
+ ### New Features
14
+
15
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
16
+ - `mergeCounter()` - track occurrences
17
+ - `mergeAverage()` - track sum/count, compute avg
18
+ - `mergeRate()` - track success/total, compute %
19
+ - `mergeMinMax()` - track min/max values
20
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
21
+
22
+ ### Improvements
23
+
24
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
25
+ - Prepares for real-time hourly aggregation without batch accumulation
26
+
27
+ - Updated dependencies [f676e11]
28
+ - Updated dependencies [48c2080]
29
+ - @checkstack/common@0.6.2
30
+ - @checkstack/backend-api@0.6.0
31
+ - @checkstack/healthcheck-common@0.8.2
32
+
3
33
  ## 0.1.13
4
34
 
5
35
  ### 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.1.14",
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,7 +112,8 @@ 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
118
  expect(aggregated.avgDaysRemaining).toBe(45);
118
119
  expect(aggregated.validRate).toBe(100);
@@ -153,7 +154,8 @@ 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
160
  expect(aggregated.validRate).toBe(50);
159
161
  });
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ averageStateSchema,
9
+ mergeRate,
10
+ rateStateSchema,
11
+ type AverageState,
12
+ type RateState,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -58,7 +64,7 @@ const certificateResultSchema = healthResultSchema({
58
64
 
59
65
  export type CertificateResult = z.infer<typeof certificateResultSchema>;
60
66
 
61
- const certificateAggregatedSchema = healthResultSchema({
67
+ const certificateAggregatedDisplaySchema = healthResultSchema({
62
68
  avgDaysRemaining: healthResultNumber({
63
69
  "x-chart-type": "gauge",
64
70
  "x-chart-label": "Avg Days Remaining",
@@ -71,6 +77,17 @@ const certificateAggregatedSchema = healthResultSchema({
71
77
  }),
72
78
  });
73
79
 
80
+ const certificateAggregatedInternalSchema = z.object({
81
+ _daysRemaining: averageStateSchema
82
+ .optional(),
83
+ _valid: rateStateSchema
84
+ .optional(),
85
+ });
86
+
87
+ const certificateAggregatedSchema = certificateAggregatedDisplaySchema.merge(
88
+ certificateAggregatedInternalSchema,
89
+ );
90
+
74
91
  export type CertificateAggregatedResult = z.infer<
75
92
  typeof certificateAggregatedSchema
76
93
  >;
@@ -83,15 +100,12 @@ export type CertificateAggregatedResult = z.infer<
83
100
  * Built-in TLS certificate collector.
84
101
  * Returns certificate information from the TLS connection.
85
102
  */
86
- export class CertificateCollector
87
- implements
88
- CollectorStrategy<
89
- TlsTransportClient,
90
- CertificateConfig,
91
- CertificateResult,
92
- CertificateAggregatedResult
93
- >
94
- {
103
+ export class CertificateCollector implements CollectorStrategy<
104
+ TlsTransportClient,
105
+ CertificateConfig,
106
+ CertificateResult,
107
+ CertificateAggregatedResult
108
+ > {
95
109
  id = "certificate";
96
110
  displayName = "TLS Certificate";
97
111
  description = "Check TLS certificate validity and expiration";
@@ -142,30 +156,27 @@ export class CertificateCollector
142
156
  };
143
157
  }
144
158
 
145
- aggregateResult(
146
- runs: HealthCheckRunForAggregation<CertificateResult>[]
159
+ mergeResult(
160
+ existing: CertificateAggregatedResult | undefined,
161
+ run: HealthCheckRunForAggregation<CertificateResult>,
147
162
  ): CertificateAggregatedResult {
148
- const daysRemaining = runs
149
- .map((r) => r.metadata?.daysRemaining)
150
- .filter((v): v is number => typeof v === "number");
163
+ const metadata = run.metadata;
151
164
 
152
- const validResults = runs
153
- .map((r) => r.metadata?.valid)
154
- .filter((v): v is boolean => typeof v === "boolean");
165
+ const daysState = mergeAverage(
166
+ existing?._daysRemaining as AverageState | undefined,
167
+ metadata?.daysRemaining,
168
+ );
155
169
 
156
- const validCount = validResults.filter(Boolean).length;
170
+ const validState = mergeRate(
171
+ existing?._valid as RateState | undefined,
172
+ metadata?.valid,
173
+ );
157
174
 
158
175
  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,
176
+ avgDaysRemaining: daysState.avg,
177
+ validRate: validState.rate,
178
+ _daysRemaining: daysState,
179
+ _valid: validState,
169
180
  };
170
181
  }
171
182
  }
@@ -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,7 +204,8 @@ 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
210
  expect(aggregated.avgDaysUntilExpiry).toBe(25);
210
211
  expect(aggregated.minDaysUntilExpiry).toBe(20);
@@ -252,7 +253,8 @@ 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
259
  expect(aggregated.invalidCount).toBe(2);
258
260
  expect(aggregated.errorCount).toBe(1);
package/src/strategy.ts CHANGED
@@ -5,6 +5,14 @@ import {
5
5
  Versioned,
6
6
  z,
7
7
  type ConnectedClient,
8
+ mergeAverage,
9
+ averageStateSchema,
10
+ mergeCounter,
11
+ counterStateSchema,
12
+ mergeMinMax,
13
+ type AverageState,
14
+ type CounterState,
15
+ type MinMaxState,
8
16
  } from "@checkstack/backend-api";
9
17
  import {
10
18
  healthResultBoolean,
@@ -83,7 +91,7 @@ type TlsResult = z.infer<typeof tlsResultSchema>;
83
91
  /**
84
92
  * Aggregated metadata for buckets.
85
93
  */
86
- const tlsAggregatedSchema = healthResultSchema({
94
+ const tlsAggregatedDisplaySchema = healthResultSchema({
87
95
  avgDaysUntilExpiry: healthResultNumber({
88
96
  "x-chart-type": "line",
89
97
  "x-chart-label": "Avg Days Until Expiry",
@@ -104,6 +112,19 @@ const tlsAggregatedSchema = healthResultSchema({
104
112
  }),
105
113
  });
106
114
 
115
+ const tlsAggregatedInternalSchema = z.object({
116
+ _daysUntilExpiry: averageStateSchema.optional(),
117
+ _minDaysUntilExpiry: z
118
+ .object({ min: z.number(), max: z.number() })
119
+ .optional(),
120
+ _invalid: counterStateSchema.optional(),
121
+ _errors: counterStateSchema.optional(),
122
+ });
123
+
124
+ const tlsAggregatedSchema = tlsAggregatedDisplaySchema.merge(
125
+ tlsAggregatedInternalSchema,
126
+ );
127
+
107
128
  type TlsAggregatedResult = z.infer<typeof tlsAggregatedSchema>;
108
129
 
109
130
  // ============================================================================
@@ -156,7 +177,7 @@ const defaultTlsClient: TlsClient = {
156
177
  getCipher: () => socket.getCipher(),
157
178
  end: () => socket.end(),
158
179
  });
159
- }
180
+ },
160
181
  );
161
182
 
162
183
  socket.on("error", reject);
@@ -172,15 +193,12 @@ const defaultTlsClient: TlsClient = {
172
193
  // STRATEGY
173
194
  // ============================================================================
174
195
 
175
- export class TlsHealthCheckStrategy
176
- implements
177
- HealthCheckStrategy<
178
- TlsConfig,
179
- TlsTransportClient,
180
- TlsResult,
181
- TlsAggregatedResult
182
- >
183
- {
196
+ export class TlsHealthCheckStrategy implements HealthCheckStrategy<
197
+ TlsConfig,
198
+ TlsTransportClient,
199
+ TlsResult,
200
+ TlsAggregatedResult
201
+ > {
184
202
  id = "tls";
185
203
  displayName = "TLS/SSL Health Check";
186
204
  description = "SSL/TLS certificate validation and expiry monitoring";
@@ -222,50 +240,46 @@ export class TlsHealthCheckStrategy
222
240
  schema: tlsAggregatedSchema,
223
241
  });
224
242
 
225
- aggregateResult(
226
- runs: HealthCheckRunForAggregation<TlsResult>[]
243
+ mergeResult(
244
+ existing: TlsAggregatedResult | undefined,
245
+ run: HealthCheckRunForAggregation<TlsResult>,
227
246
  ): TlsAggregatedResult {
228
- const validRuns = runs.filter((r) => r.metadata);
229
-
230
- if (validRuns.length === 0) {
231
- return {
232
- avgDaysUntilExpiry: 0,
233
- minDaysUntilExpiry: 0,
234
- invalidCount: 0,
235
- errorCount: 0,
236
- };
237
- }
247
+ const metadata = run.metadata;
238
248
 
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;
249
+ const daysState = mergeAverage(
250
+ existing?._daysUntilExpiry as AverageState | undefined,
251
+ metadata?.daysUntilExpiry,
252
+ );
247
253
 
248
- const minDaysUntilExpiry =
249
- daysValues.length > 0 ? Math.min(...daysValues) : 0;
254
+ const minDaysState = mergeMinMax(
255
+ existing?._minDaysUntilExpiry as MinMaxState | undefined,
256
+ metadata?.daysUntilExpiry,
257
+ );
250
258
 
251
- const invalidCount = validRuns.filter(
252
- (r) => r.metadata?.isValid === false
253
- ).length;
259
+ const invalidState = mergeCounter(
260
+ existing?._invalid as CounterState | undefined,
261
+ metadata?.isValid === false,
262
+ );
254
263
 
255
- const errorCount = validRuns.filter(
256
- (r) => r.metadata?.error !== undefined
257
- ).length;
264
+ const errorState = mergeCounter(
265
+ existing?._errors as CounterState | undefined,
266
+ metadata?.error !== undefined,
267
+ );
258
268
 
259
269
  return {
260
- avgDaysUntilExpiry,
261
- minDaysUntilExpiry,
262
- invalidCount,
263
- errorCount,
270
+ avgDaysUntilExpiry: daysState.avg,
271
+ minDaysUntilExpiry: minDaysState.min,
272
+ invalidCount: invalidState.count,
273
+ errorCount: errorState.count,
274
+ _daysUntilExpiry: daysState,
275
+ _minDaysUntilExpiry: minDaysState,
276
+ _invalid: invalidState,
277
+ _errors: errorState,
264
278
  };
265
279
  }
266
280
 
267
281
  async createClient(
268
- config: TlsConfig
282
+ config: TlsConfig,
269
283
  ): Promise<ConnectedClient<TlsTransportClient>> {
270
284
  const validatedConfig = this.config.validate(config);
271
285
 
@@ -280,7 +294,7 @@ export class TlsHealthCheckStrategy
280
294
  const cert = connection.getPeerCertificate();
281
295
  const validTo = new Date(cert.valid_to);
282
296
  const daysUntilExpiry = Math.floor(
283
- (validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
297
+ (validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24),
284
298
  );
285
299
 
286
300
  const certInfo: TlsCertificateInfo = {