@openstatus/health-tls 0.1.4-dev.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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @openstatus/health-tls
2
+
3
+ TLS probe for [`@openstatus/health`](https://jsr.io/@openstatus/health).
4
+ Completes a TLS handshake with `host:port`, requires the certificate to
5
+ chain to a trusted CA and fails the check when it expires within
6
+ `minDaysValid` days — so a certificate that is about to lapse turns the
7
+ report `degraded` two weeks before browsers start refusing it.
8
+
9
+ ```sh
10
+ deno add jsr:@openstatus/health jsr:@openstatus/health-tls
11
+ npm install @openstatus/health @openstatus/health-tls
12
+ ```
13
+
14
+ ```ts
15
+ import { createHealthHandler } from "@openstatus/health";
16
+ import { tlsProbe } from "@openstatus/health-tls";
17
+
18
+ Deno.serve(
19
+ createHealthHandler({
20
+ probes: [
21
+ tlsProbe({ host: "api.example.com" }),
22
+ tlsProbe({ name: "db-tls", host: "db.example.com", port: 5432 }),
23
+ ],
24
+ }),
25
+ );
26
+ ```
27
+
28
+ `host` is sent as the SNI server name, so the probe sees the same
29
+ certificate a client would. The socket is closed as soon as the handshake
30
+ completes; no application data is sent. A self-signed or mis-chained
31
+ certificate fails with the runtime's verification error
32
+ (`SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED`, …), and one that is
33
+ about to expire fails with `TlsCertificateExpiryError`, which carries
34
+ `expiresAt` and `daysLeft`.
35
+
36
+ Point it at your own public hostname to catch a renewal that did not run,
37
+ or at a dependency to be warned before its certificate takes you down.
38
+ Give each probe a `name` when you mount more than one.
39
+
40
+ ```ts
41
+ tlsProbe({
42
+ host: "api.example.com",
43
+ port: 443,
44
+ minDaysValid: 30,
45
+ // optional overrides from the Probe contract
46
+ name: "certificate",
47
+ critical: false,
48
+ timeoutMs: 3000,
49
+ skip: () => env.NODE_ENV !== "production",
50
+ });
51
+ ```
52
+
53
+ Non-critical by default: an expiring certificate is something to act on,
54
+ not a reason to take the instance out of rotation today. Set
55
+ `critical: true` when the handshake is with a dependency requests cannot
56
+ proceed without.
57
+
58
+ This probe uses `node:tls`, so it runs on Node.js, Deno and Bun but not on
59
+ edge runtimes without a socket API. `connect` is typed structurally and can
60
+ be replaced for tests.
61
+
62
+ ## About openstatus
63
+
64
+ [openstatus](https://www.openstatus.dev/) is the open-source uptime monitoring
65
+ and status page platform. This package is part of
66
+ [`@openstatus/health`](https://github.com/openstatusHQ/health), the `/health`
67
+ endpoints behind openstatus's own services, extracted so any JavaScript server
68
+ can expose one. Point an
69
+ [openstatus monitor](https://www.openstatus.dev/docs/reference/http-monitor)
70
+ at the endpoint and assert on `status` in the body to be alerted on
71
+ `degraded` before it becomes `unhealthy`.
72
+
73
+ Source: [github.com/openstatusHQ/health](https://github.com/openstatusHQ/health).
74
+ Issues and PRs welcome.
75
+
76
+ ## License
77
+
78
+ [MIT](https://github.com/openstatusHQ/health/blob/main/LICENSE)
@@ -0,0 +1,30 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+
25
+ Object.defineProperty(exports, '__toESM', {
26
+ enumerable: true,
27
+ get: function () {
28
+ return __toESM;
29
+ }
30
+ });
package/dist/mod.cjs ADDED
@@ -0,0 +1,88 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const node_tls = require_rolldown_runtime.__toESM(require("node:tls"));
3
+ const __openstatus_health = require_rolldown_runtime.__toESM(require("@openstatus/health"));
4
+
5
+ //#region src/mod.ts
6
+ /** Probe name when `name` is unset. */
7
+ const tlsDefaultName = "tls";
8
+ /** Port when `port` is unset. */
9
+ const tlsDefaultPort = 443;
10
+ /** Minimum remaining validity when `minDaysValid` is unset. */
11
+ const tlsDefaultMinDaysValid = 14;
12
+ /** Thrown by the probe when the certificate expires within `minDaysValid` days. */
13
+ var TlsCertificateExpiryError = class extends Error {
14
+ /** When the certificate expires. */
15
+ expiresAt;
16
+ /** Days left, rounded down; negative once expired. */
17
+ daysLeft;
18
+ /** Build the error for a certificate expiring at `expiresAt` against `minDaysValid`. */
19
+ constructor(expiresAt, daysLeft, minDaysValid) {
20
+ super(daysLeft < 0 ? `certificate expired ${expiresAt.toISOString()}` : `certificate expires in ${daysLeft} days, fewer than ${minDaysValid}`);
21
+ this.name = "TlsCertificateExpiryError";
22
+ this.expiresAt = expiresAt;
23
+ this.daysLeft = daysLeft;
24
+ }
25
+ };
26
+ /** A probe that completes a TLS handshake and checks certificate trust and expiry; non-critical by default. Throws `ProbeConfigError` for an empty `host`, an invalid `port` or a negative `minDaysValid`. */
27
+ function tlsProbe(options) {
28
+ const host = options.host;
29
+ if (typeof host !== "string" || host.length === 0) throw new __openstatus_health.ProbeConfigError("tlsProbe", "host", typeof host !== "string" ? `must be a string, got ${String(host)}` : "must not be empty");
30
+ const port = options.port ?? tlsDefaultPort;
31
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new __openstatus_health.ProbeConfigError("tlsProbe", "port", `must be an integer between 1 and 65535, got ${String(port)}`);
32
+ const minDaysValid = options.minDaysValid ?? tlsDefaultMinDaysValid;
33
+ if (!Number.isFinite(minDaysValid) || minDaysValid < 0) throw new __openstatus_health.ProbeConfigError("tlsProbe", "minDaysValid", `must be a non-negative number, got ${String(minDaysValid)}`);
34
+ const connect = options.connect ?? node_tls.connect;
35
+ return {
36
+ name: options.name ?? tlsDefaultName,
37
+ critical: options.critical ?? false,
38
+ timeoutMs: options.timeoutMs,
39
+ skip: options.skip,
40
+ run: (signal) => new Promise((resolve, reject) => {
41
+ const socket = connect({
42
+ host,
43
+ port,
44
+ servername: host
45
+ });
46
+ const settle = (finish) => {
47
+ signal.removeEventListener("abort", onAbort);
48
+ socket.destroy();
49
+ finish();
50
+ };
51
+ const onAbort = () => settle(() => reject(signal.reason));
52
+ socket.once("secureConnect", () => {
53
+ let result;
54
+ try {
55
+ result = inspect(socket, minDaysValid);
56
+ } catch (error) {
57
+ settle(() => reject(error));
58
+ return;
59
+ }
60
+ settle(() => resolve(result));
61
+ });
62
+ socket.once("error", (error) => settle(() => reject(error ?? /* @__PURE__ */ new Error("socket error"))));
63
+ signal.addEventListener("abort", onAbort, { once: true });
64
+ })
65
+ };
66
+ }
67
+ function inspect(socket, minDaysValid) {
68
+ if (!socket.authorized) {
69
+ const reason = socket.authorizationError;
70
+ throw reason instanceof Error ? reason : new Error(reason == null ? "certificate not trusted" : String(reason));
71
+ }
72
+ const expiresAt = new Date(socket.getPeerCertificate().valid_to);
73
+ if (Number.isNaN(expiresAt.getTime())) throw new Error("certificate has no readable expiry");
74
+ const msLeft = expiresAt.getTime() - Date.now();
75
+ const daysLeft = Math.floor(msLeft / 864e5);
76
+ if (msLeft < minDaysValid * 864e5) throw new TlsCertificateExpiryError(expiresAt, daysLeft, minDaysValid);
77
+ return {
78
+ expiresAt: expiresAt.toISOString(),
79
+ daysLeft
80
+ };
81
+ }
82
+
83
+ //#endregion
84
+ exports.TlsCertificateExpiryError = TlsCertificateExpiryError;
85
+ exports.tlsDefaultMinDaysValid = tlsDefaultMinDaysValid;
86
+ exports.tlsDefaultName = tlsDefaultName;
87
+ exports.tlsDefaultPort = tlsDefaultPort;
88
+ exports.tlsProbe = tlsProbe;
package/dist/mod.d.cts ADDED
@@ -0,0 +1,60 @@
1
+ import { Probe, ProbeOverrides } from "@openstatus/health";
2
+
3
+ //#region src/mod.d.ts
4
+
5
+ /** Probe name when `name` is unset. */
6
+ declare const tlsDefaultName = "tls";
7
+ /** Port when `port` is unset. */
8
+ declare const tlsDefaultPort = 443;
9
+ /** Minimum remaining validity when `minDaysValid` is unset. */
10
+ declare const tlsDefaultMinDaysValid = 14;
11
+ /** The subset of a peer certificate the probe reads. */
12
+ interface TlsPeerCertificate {
13
+ /** Expiry, as the date string `getPeerCertificate()` returns. */
14
+ readonly valid_to: string;
15
+ }
16
+ /** The subset of a `tls.TLSSocket` the probe uses. */
17
+ interface TlsLikeSocket {
18
+ /** Listen once for `secureConnect` (handshake done) or `error` (failed). */
19
+ once(event: "secureConnect" | "error", listener: (error?: Error) => void): TlsLikeSocket;
20
+ /** Whether the peer certificate chained to a trusted CA. */
21
+ readonly authorized: boolean;
22
+ /** Why it did not, when `authorized` is false. */
23
+ readonly authorizationError?: Error | string | null;
24
+ /** The peer certificate after the handshake. */
25
+ getPeerCertificate(): TlsPeerCertificate;
26
+ /** Close the socket. */
27
+ destroy(): void;
28
+ }
29
+ /** What the probe connects with; `tls.connect` by default. */
30
+ type TlsConnect = (options: {
31
+ readonly host: string;
32
+ readonly port: number;
33
+ readonly servername: string;
34
+ }) => TlsLikeSocket;
35
+ /** Options for `tlsProbe()`. */
36
+ interface TlsProbeOptions extends ProbeOverrides {
37
+ /** Hostname; also sent as the SNI server name. */
38
+ readonly host: string;
39
+ /** Port. Default `tlsDefaultPort`. */
40
+ readonly port?: number;
41
+ /** Fail when the certificate expires in fewer days than this. Default `tlsDefaultMinDaysValid`. */
42
+ readonly minDaysValid?: number;
43
+ /** Replacement `connect`, for tests. */
44
+ readonly connect?: TlsConnect;
45
+ }
46
+ /** Thrown by the probe when the certificate expires within `minDaysValid` days. */
47
+ declare class TlsCertificateExpiryError extends Error {
48
+ /** When the certificate expires. */
49
+ readonly expiresAt: Date;
50
+ /** Days left, rounded down; negative once expired. */
51
+ readonly daysLeft: number;
52
+ /** Build the error for a certificate expiring at `expiresAt` against `minDaysValid`. */
53
+ constructor(expiresAt: Date, daysLeft: number, minDaysValid: number);
54
+ }
55
+ /** A probe that completes a TLS handshake and checks certificate trust and expiry; non-critical by default. Throws `ProbeConfigError` for an empty `host`, an invalid `port` or a negative `minDaysValid`. */
56
+ declare function tlsProbe(options: TlsProbeOptions): Probe;
57
+ //# sourceMappingURL=mod.d.ts.map
58
+ //#endregion
59
+ export { TlsCertificateExpiryError, TlsConnect, TlsLikeSocket, TlsPeerCertificate, TlsProbeOptions, tlsDefaultMinDaysValid, tlsDefaultName, tlsDefaultPort, tlsProbe };
60
+ //# sourceMappingURL=mod.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.cts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;AAuEqB,cA/CR,cAAA,GA+CQ,KAAA;;AARkC,cArC1C,cAAA,GAqC0C,GAAA;AAYvD;AAAuC,cA/C1B,sBAAA,GA+C0B,EAAA;;AAOd,UAnDR,kBAAA,CAmDQ;EAAI;EAPuB,SAAA,QAAA,EAAA,MAAA;AAoBpD;;AAAkC,UA1DjB,aAAA,CA0DiB;EAAe;EAAQ,IAAA,CAAA,KAAA,EAAA,eAAA,GAAA,OAAA,EAAA,QAAA,EAAA,CAAA,KAAA,CAAA,EAtDlC,KAsDkC,EAAA,GAAA,IAAA,CAAA,EArDpD,aAqDoD;;;;gCAjDzB;;wBAER;;;;;KAMZ,UAAA;;;;MAMP;;UAGY,eAAA,SAAwB;;;;;;;;qBAQpB;;;cAIR,yBAAA,SAAkC,KAAA;;sBAEzB;;;;yBAKG;;;iBAaT,QAAA,UAAkB,kBAAkB"}
package/dist/mod.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { Probe, ProbeOverrides } from "@openstatus/health";
2
+
3
+ //#region src/mod.d.ts
4
+
5
+ /** Probe name when `name` is unset. */
6
+ declare const tlsDefaultName = "tls";
7
+ /** Port when `port` is unset. */
8
+ declare const tlsDefaultPort = 443;
9
+ /** Minimum remaining validity when `minDaysValid` is unset. */
10
+ declare const tlsDefaultMinDaysValid = 14;
11
+ /** The subset of a peer certificate the probe reads. */
12
+ interface TlsPeerCertificate {
13
+ /** Expiry, as the date string `getPeerCertificate()` returns. */
14
+ readonly valid_to: string;
15
+ }
16
+ /** The subset of a `tls.TLSSocket` the probe uses. */
17
+ interface TlsLikeSocket {
18
+ /** Listen once for `secureConnect` (handshake done) or `error` (failed). */
19
+ once(event: "secureConnect" | "error", listener: (error?: Error) => void): TlsLikeSocket;
20
+ /** Whether the peer certificate chained to a trusted CA. */
21
+ readonly authorized: boolean;
22
+ /** Why it did not, when `authorized` is false. */
23
+ readonly authorizationError?: Error | string | null;
24
+ /** The peer certificate after the handshake. */
25
+ getPeerCertificate(): TlsPeerCertificate;
26
+ /** Close the socket. */
27
+ destroy(): void;
28
+ }
29
+ /** What the probe connects with; `tls.connect` by default. */
30
+ type TlsConnect = (options: {
31
+ readonly host: string;
32
+ readonly port: number;
33
+ readonly servername: string;
34
+ }) => TlsLikeSocket;
35
+ /** Options for `tlsProbe()`. */
36
+ interface TlsProbeOptions extends ProbeOverrides {
37
+ /** Hostname; also sent as the SNI server name. */
38
+ readonly host: string;
39
+ /** Port. Default `tlsDefaultPort`. */
40
+ readonly port?: number;
41
+ /** Fail when the certificate expires in fewer days than this. Default `tlsDefaultMinDaysValid`. */
42
+ readonly minDaysValid?: number;
43
+ /** Replacement `connect`, for tests. */
44
+ readonly connect?: TlsConnect;
45
+ }
46
+ /** Thrown by the probe when the certificate expires within `minDaysValid` days. */
47
+ declare class TlsCertificateExpiryError extends Error {
48
+ /** When the certificate expires. */
49
+ readonly expiresAt: Date;
50
+ /** Days left, rounded down; negative once expired. */
51
+ readonly daysLeft: number;
52
+ /** Build the error for a certificate expiring at `expiresAt` against `minDaysValid`. */
53
+ constructor(expiresAt: Date, daysLeft: number, minDaysValid: number);
54
+ }
55
+ /** A probe that completes a TLS handshake and checks certificate trust and expiry; non-critical by default. Throws `ProbeConfigError` for an empty `host`, an invalid `port` or a negative `minDaysValid`. */
56
+ declare function tlsProbe(options: TlsProbeOptions): Probe;
57
+ //# sourceMappingURL=mod.d.ts.map
58
+ //#endregion
59
+ export { TlsCertificateExpiryError, TlsConnect, TlsLikeSocket, TlsPeerCertificate, TlsProbeOptions, tlsDefaultMinDaysValid, tlsDefaultName, tlsDefaultPort, tlsProbe };
60
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;AAuEqB,cA/CR,cAAA,GA+CQ,KAAA;;AARkC,cArC1C,cAAA,GAqC0C,GAAA;AAYvD;AAAuC,cA/C1B,sBAAA,GA+C0B,EAAA;;AAOd,UAnDR,kBAAA,CAmDQ;EAAI;EAPuB,SAAA,QAAA,EAAA,MAAA;AAoBpD;;AAAkC,UA1DjB,aAAA,CA0DiB;EAAe;EAAQ,IAAA,CAAA,KAAA,EAAA,eAAA,GAAA,OAAA,EAAA,QAAA,EAAA,CAAA,KAAA,CAAA,EAtDlC,KAsDkC,EAAA,GAAA,IAAA,CAAA,EArDpD,aAqDoD;;;;gCAjDzB;;wBAER;;;;;KAMZ,UAAA;;;;MAMP;;UAGY,eAAA,SAAwB;;;;;;;;qBAQpB;;;cAIR,yBAAA,SAAkC,KAAA;;sBAEzB;;;;yBAKG;;;iBAaT,QAAA,UAAkB,kBAAkB"}
package/dist/mod.js ADDED
@@ -0,0 +1,84 @@
1
+ import { connect } from "node:tls";
2
+ import { ProbeConfigError } from "@openstatus/health";
3
+
4
+ //#region src/mod.ts
5
+ /** Probe name when `name` is unset. */
6
+ const tlsDefaultName = "tls";
7
+ /** Port when `port` is unset. */
8
+ const tlsDefaultPort = 443;
9
+ /** Minimum remaining validity when `minDaysValid` is unset. */
10
+ const tlsDefaultMinDaysValid = 14;
11
+ /** Thrown by the probe when the certificate expires within `minDaysValid` days. */
12
+ var TlsCertificateExpiryError = class extends Error {
13
+ /** When the certificate expires. */
14
+ expiresAt;
15
+ /** Days left, rounded down; negative once expired. */
16
+ daysLeft;
17
+ /** Build the error for a certificate expiring at `expiresAt` against `minDaysValid`. */
18
+ constructor(expiresAt, daysLeft, minDaysValid) {
19
+ super(daysLeft < 0 ? `certificate expired ${expiresAt.toISOString()}` : `certificate expires in ${daysLeft} days, fewer than ${minDaysValid}`);
20
+ this.name = "TlsCertificateExpiryError";
21
+ this.expiresAt = expiresAt;
22
+ this.daysLeft = daysLeft;
23
+ }
24
+ };
25
+ /** A probe that completes a TLS handshake and checks certificate trust and expiry; non-critical by default. Throws `ProbeConfigError` for an empty `host`, an invalid `port` or a negative `minDaysValid`. */
26
+ function tlsProbe(options) {
27
+ const host = options.host;
28
+ if (typeof host !== "string" || host.length === 0) throw new ProbeConfigError("tlsProbe", "host", typeof host !== "string" ? `must be a string, got ${String(host)}` : "must not be empty");
29
+ const port = options.port ?? tlsDefaultPort;
30
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new ProbeConfigError("tlsProbe", "port", `must be an integer between 1 and 65535, got ${String(port)}`);
31
+ const minDaysValid = options.minDaysValid ?? tlsDefaultMinDaysValid;
32
+ if (!Number.isFinite(minDaysValid) || minDaysValid < 0) throw new ProbeConfigError("tlsProbe", "minDaysValid", `must be a non-negative number, got ${String(minDaysValid)}`);
33
+ const connect$1 = options.connect ?? connect;
34
+ return {
35
+ name: options.name ?? tlsDefaultName,
36
+ critical: options.critical ?? false,
37
+ timeoutMs: options.timeoutMs,
38
+ skip: options.skip,
39
+ run: (signal) => new Promise((resolve, reject) => {
40
+ const socket = connect$1({
41
+ host,
42
+ port,
43
+ servername: host
44
+ });
45
+ const settle = (finish) => {
46
+ signal.removeEventListener("abort", onAbort);
47
+ socket.destroy();
48
+ finish();
49
+ };
50
+ const onAbort = () => settle(() => reject(signal.reason));
51
+ socket.once("secureConnect", () => {
52
+ let result;
53
+ try {
54
+ result = inspect(socket, minDaysValid);
55
+ } catch (error) {
56
+ settle(() => reject(error));
57
+ return;
58
+ }
59
+ settle(() => resolve(result));
60
+ });
61
+ socket.once("error", (error) => settle(() => reject(error ?? /* @__PURE__ */ new Error("socket error"))));
62
+ signal.addEventListener("abort", onAbort, { once: true });
63
+ })
64
+ };
65
+ }
66
+ function inspect(socket, minDaysValid) {
67
+ if (!socket.authorized) {
68
+ const reason = socket.authorizationError;
69
+ throw reason instanceof Error ? reason : new Error(reason == null ? "certificate not trusted" : String(reason));
70
+ }
71
+ const expiresAt = new Date(socket.getPeerCertificate().valid_to);
72
+ if (Number.isNaN(expiresAt.getTime())) throw new Error("certificate has no readable expiry");
73
+ const msLeft = expiresAt.getTime() - Date.now();
74
+ const daysLeft = Math.floor(msLeft / 864e5);
75
+ if (msLeft < minDaysValid * 864e5) throw new TlsCertificateExpiryError(expiresAt, daysLeft, minDaysValid);
76
+ return {
77
+ expiresAt: expiresAt.toISOString(),
78
+ daysLeft
79
+ };
80
+ }
81
+
82
+ //#endregion
83
+ export { TlsCertificateExpiryError, tlsDefaultMinDaysValid, tlsDefaultName, tlsDefaultPort, tlsProbe };
84
+ //# sourceMappingURL=mod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.js","names":["expiresAt: Date","daysLeft: number","minDaysValid: number","options: TlsProbeOptions","connect","tlsConnect","finish: () => void","result: { expiresAt: string; daysLeft: number }","socket: TlsLikeSocket"],"sources":["../src/mod.ts"],"sourcesContent":["/**\n * TLS probe for `@openstatus/health`: completes a handshake with\n * `host:port`, requires a trusted certificate and fails when it expires\n * within `minDaysValid` days.\n *\n * ```ts\n * import { tlsProbe } from \"@openstatus/health-tls\";\n *\n * const probe = tlsProbe({ host: \"api.example.com\" });\n * ```\n *\n * Node.js, Deno and Bun only: it uses `node:tls`.\n *\n * @module\n */\n\nimport { connect as tlsConnect } from \"node:tls\";\nimport {\n type Probe,\n ProbeConfigError,\n type ProbeOverrides,\n} from \"@openstatus/health\";\n\n/** Probe name when `name` is unset. */\nexport const tlsDefaultName = \"tls\";\n/** Port when `port` is unset. */\nexport const tlsDefaultPort = 443;\n/** Minimum remaining validity when `minDaysValid` is unset. */\nexport const tlsDefaultMinDaysValid = 14;\n\n/** The subset of a peer certificate the probe reads. */\nexport interface TlsPeerCertificate {\n /** Expiry, as the date string `getPeerCertificate()` returns. */\n readonly valid_to: string;\n}\n\n/** The subset of a `tls.TLSSocket` the probe uses. */\nexport interface TlsLikeSocket {\n /** Listen once for `secureConnect` (handshake done) or `error` (failed). */\n once(\n event: \"secureConnect\" | \"error\",\n listener: (error?: Error) => void,\n ): TlsLikeSocket;\n /** Whether the peer certificate chained to a trusted CA. */\n readonly authorized: boolean;\n /** Why it did not, when `authorized` is false. */\n readonly authorizationError?: Error | string | null;\n /** The peer certificate after the handshake. */\n getPeerCertificate(): TlsPeerCertificate;\n /** Close the socket. */\n destroy(): void;\n}\n\n/** What the probe connects with; `tls.connect` by default. */\nexport type TlsConnect = (\n options: {\n readonly host: string;\n readonly port: number;\n readonly servername: string;\n },\n) => TlsLikeSocket;\n\n/** Options for `tlsProbe()`. */\nexport interface TlsProbeOptions extends ProbeOverrides {\n /** Hostname; also sent as the SNI server name. */\n readonly host: string;\n /** Port. Default `tlsDefaultPort`. */\n readonly port?: number;\n /** Fail when the certificate expires in fewer days than this. Default `tlsDefaultMinDaysValid`. */\n readonly minDaysValid?: number;\n /** Replacement `connect`, for tests. */\n readonly connect?: TlsConnect;\n}\n\n/** Thrown by the probe when the certificate expires within `minDaysValid` days. */\nexport class TlsCertificateExpiryError extends Error {\n /** When the certificate expires. */\n readonly expiresAt: Date;\n /** Days left, rounded down; negative once expired. */\n readonly daysLeft: number;\n\n /** Build the error for a certificate expiring at `expiresAt` against `minDaysValid`. */\n constructor(expiresAt: Date, daysLeft: number, minDaysValid: number) {\n super(\n daysLeft < 0\n ? `certificate expired ${expiresAt.toISOString()}`\n : `certificate expires in ${daysLeft} days, fewer than ${minDaysValid}`,\n );\n this.name = \"TlsCertificateExpiryError\";\n this.expiresAt = expiresAt;\n this.daysLeft = daysLeft;\n }\n}\n\n/** A probe that completes a TLS handshake and checks certificate trust and expiry; non-critical by default. Throws `ProbeConfigError` for an empty `host`, an invalid `port` or a negative `minDaysValid`. */\nexport function tlsProbe(options: TlsProbeOptions): Probe {\n const host = options.host;\n if (typeof host !== \"string\" || host.length === 0) {\n throw new ProbeConfigError(\n \"tlsProbe\",\n \"host\",\n typeof host !== \"string\"\n ? `must be a string, got ${String(host)}`\n : \"must not be empty\",\n );\n }\n const port = options.port ?? tlsDefaultPort;\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new ProbeConfigError(\n \"tlsProbe\",\n \"port\",\n `must be an integer between 1 and 65535, got ${String(port)}`,\n );\n }\n const minDaysValid = options.minDaysValid ?? tlsDefaultMinDaysValid;\n if (!Number.isFinite(minDaysValid) || minDaysValid < 0) {\n throw new ProbeConfigError(\n \"tlsProbe\",\n \"minDaysValid\",\n `must be a non-negative number, got ${String(minDaysValid)}`,\n );\n }\n const connect = options.connect ?? tlsConnect;\n return {\n name: options.name ?? tlsDefaultName,\n critical: options.critical ?? false,\n timeoutMs: options.timeoutMs,\n skip: options.skip,\n run: (signal) =>\n new Promise<{ expiresAt: string; daysLeft: number }>(\n (resolve, reject) => {\n const socket = connect({ host, port, servername: host });\n const settle = (finish: () => void) => {\n signal.removeEventListener(\"abort\", onAbort);\n socket.destroy();\n finish();\n };\n const onAbort = () => settle(() => reject(signal.reason));\n socket.once(\"secureConnect\", () => {\n let result: { expiresAt: string; daysLeft: number };\n try {\n result = inspect(socket, minDaysValid);\n } catch (error) {\n settle(() => reject(error));\n return;\n }\n settle(() => resolve(result));\n });\n socket.once(\n \"error\",\n (error) => settle(() => reject(error ?? new Error(\"socket error\"))),\n );\n signal.addEventListener(\"abort\", onAbort, { once: true });\n },\n ),\n };\n}\n\nfunction inspect(\n socket: TlsLikeSocket,\n minDaysValid: number,\n): { expiresAt: string; daysLeft: number } {\n if (!socket.authorized) {\n const reason = socket.authorizationError;\n throw reason instanceof Error\n ? reason\n : new Error(reason == null ? \"certificate not trusted\" : String(reason));\n }\n const expiresAt = new Date(socket.getPeerCertificate().valid_to);\n if (Number.isNaN(expiresAt.getTime())) {\n throw new Error(\"certificate has no readable expiry\");\n }\n const msLeft = expiresAt.getTime() - Date.now();\n const daysLeft = Math.floor(msLeft / 86_400_000);\n if (msLeft < minDaysValid * 86_400_000) {\n throw new TlsCertificateExpiryError(expiresAt, daysLeft, minDaysValid);\n }\n return { expiresAt: expiresAt.toISOString(), daysLeft };\n}\n"],"mappings":";;;;;AAwBA,MAAa,iBAAiB;;AAE9B,MAAa,iBAAiB;;AAE9B,MAAa,yBAAyB;;AA+CtC,IAAa,4BAAb,cAA+C,MAAM;;CAEnD,AAAS;;CAET,AAAS;;CAGT,YAAYA,WAAiBC,UAAkBC,cAAsB;AACnE,QACE,WAAW,KACN,sBAAsB,UAAU,aAAa,CAAC,KAC9C,yBAAyB,SAAS,oBAAoB,aAAa,EACzE;AACD,OAAK,OAAO;AACZ,OAAK,YAAY;AACjB,OAAK,WAAW;CACjB;AACF;;AAGD,SAAgB,SAASC,SAAiC;CACxD,MAAM,OAAO,QAAQ;AACrB,YAAW,SAAS,YAAY,KAAK,WAAW,EAC9C,OAAM,IAAI,iBACR,YACA,eACO,SAAS,YACX,wBAAwB,OAAO,KAAK,CAAC,IACtC;CAGR,MAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAK,OAAO,UAAU,KAAK,IAAI,OAAO,KAAK,OAAO,MAChD,OAAM,IAAI,iBACR,YACA,SACC,8CAA8C,OAAO,KAAK,CAAC;CAGhE,MAAM,eAAe,QAAQ,gBAAgB;AAC7C,MAAK,OAAO,SAAS,aAAa,IAAI,eAAe,EACnD,OAAM,IAAI,iBACR,YACA,iBACC,qCAAqC,OAAO,aAAa,CAAC;CAG/D,MAAMC,YAAU,QAAQ,WAAWC;AACnC,QAAO;EACL,MAAM,QAAQ,QAAQ;EACtB,UAAU,QAAQ,YAAY;EAC9B,WAAW,QAAQ;EACnB,MAAM,QAAQ;EACd,KAAK,CAAC,WACJ,IAAI,QACF,CAAC,SAAS,WAAW;GACnB,MAAM,SAAS,UAAQ;IAAE;IAAM;IAAM,YAAY;GAAM,EAAC;GACxD,MAAM,SAAS,CAACC,WAAuB;AACrC,WAAO,oBAAoB,SAAS,QAAQ;AAC5C,WAAO,SAAS;AAChB,YAAQ;GACT;GACD,MAAM,UAAU,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,CAAC;AACzD,UAAO,KAAK,iBAAiB,MAAM;IACjC,IAAIC;AACJ,QAAI;AACF,cAAS,QAAQ,QAAQ,aAAa;IACvC,SAAQ,OAAO;AACd,YAAO,MAAM,OAAO,MAAM,CAAC;AAC3B;IACD;AACD,WAAO,MAAM,QAAQ,OAAO,CAAC;GAC9B,EAAC;AACF,UAAO,KACL,SACA,CAAC,UAAU,OAAO,MAAM,OAAO,yBAAS,IAAI,MAAM,gBAAgB,CAAC,CACpE;AACD,UAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAM,EAAC;EAC1D;CAEN;AACF;AAED,SAAS,QACPC,QACAN,cACyC;AACzC,MAAK,OAAO,YAAY;EACtB,MAAM,SAAS,OAAO;AACtB,QAAM,kBAAkB,QACpB,SACA,IAAI,MAAM,UAAU,OAAO,4BAA4B,OAAO,OAAO;CAC1E;CACD,MAAM,YAAY,IAAI,KAAK,OAAO,oBAAoB,CAAC;AACvD,KAAI,OAAO,MAAM,UAAU,SAAS,CAAC,CACnC,OAAM,IAAI,MAAM;CAElB,MAAM,SAAS,UAAU,SAAS,GAAG,KAAK,KAAK;CAC/C,MAAM,WAAW,KAAK,MAAM,SAAS,MAAW;AAChD,KAAI,SAAS,eAAe,MAC1B,OAAM,IAAI,0BAA0B,WAAW,UAAU;AAE3D,QAAO;EAAE,WAAW,UAAU,aAAa;EAAE;CAAU;AACxD"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@openstatus/health-tls",
3
+ "version": "0.1.4-dev.0",
4
+ "description": "TLS certificate probe for @openstatus/health",
5
+ "keywords": [
6
+ "openstatus",
7
+ "health",
8
+ "healthcheck",
9
+ "tls",
10
+ "ssl",
11
+ "certificate",
12
+ "expiry"
13
+ ],
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "openstatus",
17
+ "url": "https://www.openstatus.dev/"
18
+ },
19
+ "homepage": "https://github.com/openstatusHQ/health",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/openstatusHQ/health.git",
23
+ "directory": "packages/tls/"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/openstatusHQ/health/issues"
27
+ },
28
+ "type": "module",
29
+ "module": "./dist/mod.js",
30
+ "main": "./dist/mod.cjs",
31
+ "types": "./dist/mod.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": {
35
+ "import": "./dist/mod.d.ts",
36
+ "require": "./dist/mod.d.cts"
37
+ },
38
+ "import": "./dist/mod.js",
39
+ "require": "./dist/mod.cjs"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "sideEffects": false,
44
+ "files": [
45
+ "dist/"
46
+ ],
47
+ "engines": {
48
+ "node": ">=22"
49
+ },
50
+ "peerDependencies": {
51
+ "@openstatus/health": "^0.1.4-dev.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": ">=22",
55
+ "tsdown": "^0.12.7",
56
+ "typescript": "^5.8.3"
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "prepack": "tsdown",
61
+ "test": "node --experimental-transform-types --test"
62
+ }
63
+ }