tls-check

Inspect TLS/SSL certificates in Node.js using the built-in tls module -- connect, extract cert metadata, calculate days until expiry, and handle errors.

7 stars

Best use case

tls-check is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Inspect TLS/SSL certificates in Node.js using the built-in tls module -- connect, extract cert metadata, calculate days until expiry, and handle errors.

Teams using tls-check should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/tls-check/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/devops-infrastructure/ssl-cert-monitor/skills/tls-check/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/tls-check/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How tls-check Compares

Feature / Agenttls-checkStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Inspect TLS/SSL certificates in Node.js using the built-in tls module -- connect, extract cert metadata, calculate days until expiry, and handle errors.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

## When to use this skill

Use this skill when implementing certificate inspection in Node.js without external HTTP clients: connecting via TLS, extracting expiry dates and issuer information, handling timeouts and errors, and calculating days remaining.

## Install

No additional packages needed. Uses Node.js built-in `tls` module.

## Basic TLS Connect and Cert Extraction

```typescript
import tls from 'node:tls';

interface CertInfo {
  expiresAt: Date;
  issuedAt: Date;
  issuer: string;
  subject: string;
  sans: string[];
  serialNumber: string;
  daysUntilExpiry: number;
}

function checkCert(hostname: string, port = 443): Promise<CertInfo> {
  return new Promise((resolve, reject) => {
    const socket = tls.connect(
      {
        host: hostname,
        port,
        servername: hostname,        // SNI: required for virtual hosting
        rejectUnauthorized: false,   // inspect expired/self-signed certs too
      },
      () => {
        const cert = socket.getPeerCertificate(false);
        socket.end();

        if (!cert || !cert.valid_to) {
          reject(new Error('No certificate in TLS handshake'));
          return;
        }

        const expiresAt = new Date(cert.valid_to);
        const issuedAt = new Date(cert.valid_from);
        const now = new Date();
        const daysUntilExpiry = Math.floor(
          (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
        );

        resolve({
          expiresAt,
          issuedAt,
          issuer: cert.issuer?.O ?? cert.issuer?.CN ?? '',
          subject: cert.subject?.CN ?? '',
          sans: parseSANs(cert.subjectaltname ?? ''),
          serialNumber: cert.serialNumber ?? '',
          daysUntilExpiry,
        });
      },
    );

    socket.setTimeout(10_000, () => {
      socket.destroy();
      reject(new Error('Connection timeout after 10000ms'));
    });

    socket.on('error', (err) => {
      reject(err);
    });
  });
}

function parseSANs(subjectaltname: string): string[] {
  if (!subjectaltname) return [];
  return subjectaltname
    .split(', ')
    .filter((s) => s.startsWith('DNS:'))
    .map((s) => s.replace(/^DNS:/, ''));
}
```

## Certificate Object Fields

`getPeerCertificate(false)` returns a `tls.PeerCertificate` with these useful fields:

| Field | Type | Example |
|-------|------|---------|
| `subject.CN` | string | `api.example.com` |
| `issuer.O` | string | `Let's Encrypt` |
| `issuer.CN` | string | `R3` |
| `valid_from` | string | `Jan 27 00:00:00 2024 GMT` |
| `valid_to` | string | `Apr 27 00:00:00 2024 GMT` |
| `subjectaltname` | string | `DNS:api.example.com, DNS:www.api.example.com` |
| `serialNumber` | string | `04A13B9C...` (hex, uppercase) |
| `fingerprint` | string | `AA:BB:CC:...` |

`valid_from` and `valid_to` are parseable directly with `new Date()`.

## Non-Default Ports

```typescript
// SMTP with STARTTLS (not supported via tls.connect -- use separate approach)
// HTTPS on non-standard port
const info = await checkCert('api.example.com', 8443);

// LDAPS
const info = await checkCert('ldap.corp.net', 636);
```

## Handling Expired Certificates

`rejectUnauthorized: false` allows the handshake to complete even if:
- The certificate is already expired
- The certificate is self-signed
- The chain is incomplete

This means `daysUntilExpiry` will be negative for expired certs. Store as-is; the UI renders as "Expired".

## Concurrency Limiter

```typescript
import { checkCert } from './checker.js';

async function checkAll(hosts: Array<{ hostname: string; port: number }>, concurrency = 5) {
  const results: Map<string, CertInfo | Error> = new Map();
  const queue = [...hosts];

  async function worker() {
    while (queue.length > 0) {
      const host = queue.shift();
      if (!host) break;
      const key = `${host.hostname}:${host.port}`;
      try {
        results.set(key, await checkCert(host.hostname, host.port));
      } catch (err) {
        results.set(key, err instanceof Error ? err : new Error(String(err)));
      }
    }
  }

  await Promise.all(Array.from({ length: concurrency }, () => worker()));
  return results;
}
```

## Checking Multiple Certificates on the Same IP

When a server hosts multiple domains, pass `servername` to use SNI:

```typescript
// Both hit the same IP but get different certificates via SNI
const cert1 = await checkCert('app.example.com', 443);
const cert2 = await checkCert('api.example.com', 443);
```

`servername` in the options object sets the SNI extension. Always set it equal to the logical hostname, not the IP.

## Error Handling

| Error Code | Cause | Action |
|------------|-------|--------|
| `ECONNREFUSED` | Nothing listening on the port | Mark as error, show in UI |
| `ECONNRESET` | Connection dropped by server | Retry once, then mark error |
| `ETIMEDOUT` | No response within timeout | Check network path; increase timeout |
| `ENOTFOUND` | DNS resolution failure | Verify hostname spelling |
| `CERT_HAS_EXPIRED` | Not thrown (rejectUnauthorized false) | `daysUntilExpiry` will be negative |
| `SELF_SIGNED_CERT` | Not thrown (rejectUnauthorized false) | Certificate is still inspectable |

## Days Until Expiry Calculation

```typescript
function daysUntilExpiry(expiresAt: Date): number {
  const now = Date.now();
  const expiry = expiresAt.getTime();
  return Math.floor((expiry - now) / (1000 * 60 * 60 * 24));
}
// Returns negative values for expired certs
// Returns 0 for certs expiring today
```

## Checking Self-Signed Certificates

```typescript
// Self-signed or private CA -- still readable with rejectUnauthorized: false
const cert = await checkCert('internal.corp.net', 443);
// cert.issuer will equal cert.subject for self-signed
const isSelfSigned = cert.issuer === cert.subject;
```

## One-Shot vs. Streaming

`tls.connect` with the callback approach is one-shot: connect, read cert, close. This is appropriate for periodic monitoring. Do not keep the socket open between checks.

Related Skills

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose

Skill: Pastebin Core

7
from heldernoid/agentic-build-templates

## Purpose