Functional Weave
Code in Rust

monitor.cert-expiry

How long a TLS certificate has left before its notAfter, and whether that is ok, a warning, critical or expired.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 15 tests, run in TypeScript, Python and Rust.

What it does

How long a TLS certificate has left, and whether to worry: `ok`, `warning`, `critical` or `expired`. The caller reads `notAfter` from the certificate (as Unix seconds); this decides what it means today.

## Decisions

For example

  • cert_expiry(1,767,225,600, 1,762,041,600, 30, 7) → state ok, seconds left 5,184,000, days left 60 sixty days left is ok
  • cert_expiry(1,767,225,600, 1,764,633,600, 30, 7) → state ok, seconds left 2,592,000, days left 30 exactly thirty days left is not fewer than thirty: still ok
  • cert_expiry(1,767,225,600, 1,764,633,601, 30, 7) → state warning, seconds left 2,591,999, days left 29 one second under thirty days is a warning

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn cert_expiry(not_after: i64, now: i64, warn_days: i64, critical_days: i64) -> CertExpiry
not_afterintthe certificate's notAfter in Unix seconds; it is still valid during that second
nowintUnix seconds
warn_daysintwarn when fewer than this many days are left, at least criticalDays
critical_daysintcritical when fewer than this many days are left, 0 or more
returnsCertExpiry

The types it declares, generated into your project

// CertExpiryState is a string in Rust, one of: "ok", "warning", "critical", "expired".
// Parameters take it as &str and results hold it as String.

/// How close a certificate is to expiring.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertExpiry {
    pub state: String,
    /// notAfter - now while valid, 0 once expired
    pub seconds_left: i64,
    /// whole days left: floor(secondsLeft / 86400)
    pub days_left: i64,
}

Your code names it in one line, in the file that uses it

fune!(monitor.cert-expiry@^1);  // then call cert_expiry(…)
impl/rust.rs · 61 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// RFC 5280 makes validity inclusive of notAfter, so a certificate is expired
/// only after that second, not at it. Thresholds compare whole days left with
/// the day counts: fewer than N days (secondsLeft < N * 86400) is exactly
/// floor(secondsLeft / 86400) < N, with no multiplication to overflow.
///
/// # Panics
/// Panics when critical_days is negative or warn_days is below critical_days.
pub fn cert_expiry(not_after: i64, now: i64, warn_days: i64, critical_days: i64) -> CertExpiry {
    if critical_days < 0 {
        panic!("criticalDays must be a whole number 0 or more, received {}", critical_days);
    }
    if warn_days < critical_days {
        panic!("warnDays must be a whole number at least criticalDays ({}), received {}", critical_days, warn_days);
    }
    if now > not_after {
        return CertExpiry { state: "expired".to_string(), seconds_left: 0, days_left: 0 };
    }
    let seconds_left = not_after - now;
    let days_left = seconds_left / 86400;
    let state = if days_left < critical_days {
        "critical"
    } else if days_left < warn_days {
        "warning"
    } else {
        "ok"
    };
    CertExpiry { state: state.to_string(), seconds_left, days_left }
}

pub fn cert_expiry_to_value(c: &CertExpiry) -> Value {
    Value::obj(vec![
        ("state", Value::str(&c.state)),
        ("secondsLeft", Value::Int(c.seconds_left)),
        ("daysLeft", Value::Int(c.days_left)),
    ])
}

fn show(v: &Value) -> String {
    match v {
        Value::Int(i) => i.to_string(),
        Value::Float(f) => f.to_string(),
        _ => "a non-number".to_string(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    if !matches!(args[0], Value::Int(_)) || !matches!(args[1], Value::Int(_)) {
        panic!("notAfter and now must be whole seconds, received {} and {}", show(&args[0]), show(&args[1]));
    }
    let critical = match &args[3] {
        Value::Int(i) => *i,
        other => panic!("criticalDays must be a whole number 0 or more, received {}", show(other)),
    };
    let warn = match &args[2] {
        Value::Int(i) => *i,
        other => panic!("warnDays must be a whole number at least criticalDays ({}), received {}", critical, show(other)),
    };
    cert_expiry_to_value(&cert_expiry(args[0].as_i64(), args[1].as_i64(), warn, critical))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add monitor.cert-expiry
Download for Rust monitor.cert-expiry-1.0.0-rust.fune · 9,587 bytes sha256 a901b93ca48f0b6ff27e4404e6484cae6c65f1d4a5582e654ac0963b71dee621

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./monitor.cert-expiry-1.0.0-rust.fune, or fetch it from a terminal with fune pull monitor.cert-expiry@1.0.0:rust.

The whole function, every language, is one file too: monitor.cert-expiry-1.0.0.fune, 12,501 bytes, sha256 292a46615a892497895595a25acb9d91861d2afa190688786e5cd8eca8dd9720. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before monitor.cert-expiry

after — your function gets the result and the arguments, and returns the final result.

// fune: after monitor.cert-expiry

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show monitor.cert-expiry --steps.

// fune: step monitor.cert-expiry after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
sixty days left is ok 1,767,225,600, 1,762,041,600, 30, 7 → state ok, seconds left 5,184,000, days left 60
exactly thirty days left is not fewer than thirty: still ok 1,767,225,600, 1,764,633,600, 30, 7 → state ok, seconds left 2,592,000, days left 30
one second under thirty days is a warning 1,767,225,600, 1,764,633,601, 30, 7 → state warning, seconds left 2,591,999, days left 29
exactly seven days left is a warning, not critical 1,767,225,600, 1,766,620,800, 30, 7 → state warning, seconds left 604,800, days left 7
one second under seven days is critical 1,767,225,600, 1,766,620,801, 30, 7 → state critical, seconds left 604,799, days left 6
the notAfter second itself is still valid (RFC 5280: inclusive) 1,767,225,600, 1,767,225,600, 30, 7 → state critical, seconds left 0, days left 0
one second after notAfter is expired 1,767,225,600, 1,767,225,601, 30, 7 → state expired, seconds left 0, days left 0
long expired shows zero, never negative 1,767,225,600, 1,853,625,600, 30, 7 → state expired, seconds left 0, days left 0
with no thresholds the last second is ok 1,767,225,600, 1,767,225,599, 0, 0 → state ok, seconds left 1, days left 0
equal warn and critical days: never a warning, straight to critical 1,767,225,600, 1,766,361,600, 14, 14 → state critical, seconds left 864,000, days left 10
Show the other 5 tests
CaseArgumentsExpected
half a day is zero whole days, critical 1,767,225,600, 1,767,182,400, 30, 1 → state critical, seconds left 43,200, days left 0
the RFC 5280 no-expiry notAfter 99991231235959Z is ok for millions of days 253,402,300,799, 1,767,225,600, 30, 7 → state ok, seconds left 251,635,075,199, days left 2,912,442
a negative criticalDays is an error 1,767,225,600, 1,767,225,600, 30, -1 → error: criticalDays must be a whole number 0 or more, received -1
warnDays below criticalDays is an error 1,767,225,600, 1,767,225,600, 7, 14 → error: warnDays must be a whole number at least criticalDays (14), received 7
a fractional now is an error 1,767,225,600, 1.5, 30, 7 → error: notAfter and now must be whole seconds, received 1767225600 and 1.5

More from the author

- **notAfter is inclusive.** RFC 5280 section 4.1.2.5: "The validity period for a certificate is the period of time from notBefore through notAfter, inclusive." So the certificate is still valid during the notAfter second and is `expired` only when `now > notAfter`. At `now == notAfter`, `secondsLeft` is 0 but the state is not yet expired. - **Thresholds are "fewer than N days"**: critical when `secondsLeft < criticalDays * 86400`, else warning when `secondsLeft < warnDays * 86400`, else ok. Exactly 30 days left with a 30-day warning is still ok. This is computed as `floor(secondsLeft / 86400) < N`, which is the same test with nothing to overflow. - `daysLeft` is whole days, rounded down: 6 days 23 hours is 6. - `secondsLeft` and `daysLeft` are 0 once expired, never negative. - `warnDays` equal to `criticalDays` means no warning stage. With `criticalDays` 0 there is no critical stage, and the last valid second is `ok`; pick at least 1 if you want to hear about it. - The usual settings are 30 and 7 days (Let's Encrypt certificates last 90 days and are renewed at 30 left), or 14 and 3. - `99991231235959Z` (253402300799), which RFC 5280 says marks a certificate with no well-defined expiry, is simply a very distant date and reads `ok`.

## Errors

- `criticalDays must be a whole number 0 or more, received X` - `warnDays must be a whole number at least criticalDays (C), received X` - `notAfter and now must be whole seconds, received A and B`

## Sources

- RFC 5280, Internet X.509 Public Key Infrastructure Certificate and CRL Profile, section 4.1.2.5 Validity: https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.5

Files

PathBytes
README.md1,908
impl/python.py1,457
impl/rust.rs2,344
impl/typescript.ts1,339
vectors.json2,598