@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 +54 -0
- package/package.json +6 -6
- package/src/certificate-collector.test.ts +8 -6
- package/src/certificate-collector.ts +31 -38
- package/src/strategy.test.ts +11 -9
- package/src/strategy.ts +44 -58
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.
|
|
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.
|
|
13
|
-
"@checkstack/common": "0.6.
|
|
14
|
-
"@checkstack/healthcheck-common": "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.
|
|
20
|
-
"@checkstack/scripts": "0.1.
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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:
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
110
|
+
aggregatedResult = new VersionedAggregated({
|
|
106
111
|
version: 1,
|
|
107
|
-
|
|
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
|
-
|
|
146
|
-
|
|
150
|
+
mergeResult(
|
|
151
|
+
existing: CertificateAggregatedResult | undefined,
|
|
152
|
+
run: HealthCheckRunForAggregation<CertificateResult>,
|
|
147
153
|
): CertificateAggregatedResult {
|
|
148
|
-
const
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -164,7 +164,7 @@ describe("TlsHealthCheckStrategy", () => {
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
describe("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
103
|
+
invalidCount: aggregatedCounter({
|
|
98
104
|
"x-chart-type": "counter",
|
|
99
105
|
"x-chart-label": "Invalid Certificates",
|
|
100
106
|
}),
|
|
101
|
-
errorCount:
|
|
107
|
+
errorCount: aggregatedCounter({
|
|
102
108
|
"x-chart-type": "counter",
|
|
103
109
|
"x-chart-label": "Errors",
|
|
104
110
|
}),
|
|
105
|
-
}
|
|
111
|
+
};
|
|
106
112
|
|
|
107
|
-
type TlsAggregatedResult =
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
223
|
+
aggregatedResult = new VersionedAggregated({
|
|
221
224
|
version: 1,
|
|
222
|
-
|
|
225
|
+
fields: tlsAggregatedFields,
|
|
223
226
|
});
|
|
224
227
|
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
mergeResult(
|
|
229
|
+
existing: TlsAggregatedResult | undefined,
|
|
230
|
+
run: HealthCheckRunForAggregation<TlsResult>,
|
|
227
231
|
): TlsAggregatedResult {
|
|
228
|
-
const
|
|
232
|
+
const metadata = run.metadata;
|
|
229
233
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
+
const minDaysUntilExpiry = mergeMinMax(
|
|
240
|
+
existing?.minDaysUntilExpiry,
|
|
241
|
+
metadata?.daysUntilExpiry,
|
|
242
|
+
);
|
|
250
243
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
).length;
|
|
244
|
+
const isInvalid = metadata?.isValid === false;
|
|
245
|
+
const invalidCount = mergeCounter(existing?.invalidCount, isInvalid);
|
|
254
246
|
|
|
255
|
-
const
|
|
256
|
-
|
|
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 = {
|