@openstatus/health-tls 0.1.4-dev.0 → 0.1.4

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/dist/mod.cjs CHANGED
@@ -38,6 +38,7 @@ function tlsProbe(options) {
38
38
  timeoutMs: options.timeoutMs,
39
39
  skip: options.skip,
40
40
  run: (signal) => new Promise((resolve, reject) => {
41
+ signal.throwIfAborted();
41
42
  const socket = connect({
42
43
  host,
43
44
  port,
package/dist/mod.js CHANGED
@@ -37,6 +37,7 @@ function tlsProbe(options) {
37
37
  timeoutMs: options.timeoutMs,
38
38
  skip: options.skip,
39
39
  run: (signal) => new Promise((resolve, reject) => {
40
+ signal.throwIfAborted();
40
41
  const socket = connect$1({
41
42
  host,
42
43
  port,
package/dist/mod.js.map CHANGED
@@ -1 +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"}
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 signal.throwIfAborted();\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;AACnB,UAAO,gBAAgB;GACvB,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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openstatus/health-tls",
3
- "version": "0.1.4-dev.0",
3
+ "version": "0.1.4",
4
4
  "description": "TLS certificate probe for @openstatus/health",
5
5
  "keywords": [
6
6
  "openstatus",
@@ -48,7 +48,7 @@
48
48
  "node": ">=22"
49
49
  },
50
50
  "peerDependencies": {
51
- "@openstatus/health": "^0.1.4-dev.0"
51
+ "@openstatus/health": "^0.1.4"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": ">=22",