Skip to content

Repository files navigation

Requisite for TypeScript

Validated confidence gates and monotonic freshness checks for TypeScript, with an optional no-dependency trust-transition wrapper.

npm install @slepp/requisite

The package ships ESM, CommonJS, declarations, source maps, and subpath exports. It has no runtime dependencies and requires Node.js 20 or later.

Confidence gates

Confident<T> keeps its value behind a private field until gate() classifies the validated probability.

import {
  Confident,
  Thresholds,
  type Certain,
} from "@slepp/requisite";

function execute(_proof: Certain, value: string): void {
  console.log(value);
}

const forecast = new Confident("wake operator", 0.98);
const thresholds = new Thresholds(0.7, 0.97);

const decision = forecast.gate(thresholds);
switch (decision.kind) {
  case "certain":
    execute(decision.proof, decision.value);
    break;
  case "likely":
    console.log("request review", decision.value);
    break;
  case "unsure":
    console.log("record only", decision.value);
}

Probabilities must be finite numbers in 0..=1. The default thresholds are 0.60 and 0.95. Custom certain thresholds may be raised but not lowered below Thresholds.MIN_CERTAIN, so every Certain token has the same minimum meaning.

Certain has a module-private type brand and runtime identity. It is issued only by the highest gate branch. isCertain(value) checks whether this copy of the module issued a token.

A token proves only that some gate in that runtime identity classified a probability at or above a valid certain threshold. It is reusable and is not bound to the gated value, the chosen threshold object, the gate invocation, or a point in time. Keep the proof and value together in application control flow.

String(new Confidence(0.75)) returns Confidence(0.75). Errors expose stable code fields for recognition across package runtime identities.

Freshness

Each Fresh<T> stores its own fetch time, TTL, and monotonic clock. A value is fresh through the exact TTL boundary. Monotonic elapsed time may exclude time while the process or machine is suspended; it is not a wall-clock expiry deadline.

import { Fresh, type MonotonicClock } from "@slepp/requisite";

let now = 100;
const clock: MonotonicClock = { now: () => now };
const quote = Fresh.capture(499, { ttlMs: 30, clock });

now = 131;
const checked = quote.read();

if (checked.status === "stale") {
  console.log(checked.stale.ageMs, checked.stale.overdueByMs);
}

read() withholds an expired value. recover() is the explicit stale-value path:

const recovered = quote.recover();
if (recovered.status === "stale") {
  persistForAudit(recovered.value, recovered.stale);
}

Fresh.at(value, { fetchedAt, ttlMs, clock }) accepts an existing timestamp from the same clock epoch and rejects future timestamps. Invalid clock readings and clock regressions have dedicated error classes.

The default clock is globalThis.performance.now(). Inject a clock for tests and for systems with another monotonic time source. A clock whose now method is directly Date.now is rejected because wall time can move backward. A wrapper such as () => Date.now() cannot be identified at runtime; do not use one.

Clock regression remains fail-closed: read(), recover(), and remainingMs() throw ClockRegressionError rather than treating a negative age as fresh.

Freshness throw paths

operation throws
Fresh.capture invalid TTL or clock, non-finite clock reading, or the clock's own exception
Fresh.at the above, non-finite fetchedAt, or future fetchedAt
read, recover, remainingMs invalid/mutated clock, non-finite reading, clock regression, or the clock's own exception

The concrete classes are InvalidTtlError, InvalidMonotonicClockError, InvalidClockReadingError, InvalidFetchedAtError, InvalidFetchTimeError, MonotonicClockUnavailableError, and ClockRegressionError.

Trust transitions

The trust API is intentionally small. Its value beside schema-library brands is runtime identity: wrapper implementations and fields are module-private, and isTrusted checks provenance at runtime.

import {
  sanitize,
  untrusted,
  type Trusted,
} from "@slepp/requisite";

function loadCustomer(id: Trusted<number>): void {
  databaseLookup(id.unwrap());
}

const raw = untrusted(" 42 ");
const id = sanitize(raw, (value) => {
  const parsed = Number(value.trim());
  if (!Number.isSafeInteger(parsed)) throw new Error("invalid id");
  return parsed;
});

loadCustomer(id);

trySanitize accepts a typed { ok: true, value } | { ok: false, error } result. That error branch represents expected policy failure. Exceptions thrown by either sanitize or trySanitize policies propagate unchanged; they are not converted into result values. Forged or foreign wrappers raise InvalidTrustInputError whose operation is "sanitize" or "trySanitize". Trusted<T>.downgrade() returns Untrusted<T>.

The callback defines the destination's policy. An identity callback proves only that an identity callback ran. If a project already uses Zod or Effect schemas and brands, those usually provide richer validation and error reporting.

Enforcement limits

TypeScript is not a security boundary.

  • any, type assertions, @ts-ignore, and untyped JavaScript can bypass static contracts.
  • The package's ESM and CommonJS builds are separate runtime identities. Package-issued Confidence, Thresholds, Certain, Trusted, and Untrusted values or tokens cannot cross between them. Root and subpath imports within one module system share an identity. Use one module system throughout a process.
  • instanceof also does not cross ESM/CommonJS identities. Use hasRequisiteErrorCode(error, REQUISITE_ERROR_CODES.invalidConfidence) when an error may cross that boundary. Error codes are discriminators, not authenticity proofs, and plain objects can spoof them.
  • Duplicate physical installations likewise have separate runtime identities.
  • Certain tokens are reusable and gate() can be called more than once; TypeScript has no affine or linear values.
  • JavaScript has no move or borrow checking. recover() cannot consume a wrapper or revoke references obtained earlier.
  • Freshness is checked only when read(), recover(), or remainingMs() is called. A previously returned object reference can later become stale.
  • There is no Live/withLive API: TypeScript callbacks cannot prevent a value from escaping through outer mutable state.

API surface

The root export and these subpaths are public:

  • @slepp/requisite/confidence
  • @slepp/requisite/errors
  • @slepp/requisite/freshness
  • @slepp/requisite/trust

Declarations document each exported type and function. See examples/payment.ts for a combined flow and CHANGELOG.md for release notes.

Development

npm ci
npm audit --audit-level=high
npm run typecheck
npm test
npm run check

Runtime tests use Vitest. test-d/ contains positive and negative compile-time contracts; removing an expected error makes tsc fail. The prepack hook rebuilds dist, so npm pack does not depend on ignored build artifacts being present.

License

Licensed under either Apache-2.0 or MIT, at your option. The package metadata uses the SPDX compound expression MIT OR Apache-2.0; see the packed LICENSE summary.

About

Typed validated values, explicit trust transitions, exhaustive confidence levels, and per-value TTL freshness for TypeScript.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages