npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mythicalos/telemetry

v0.1.2

Published

The shared mythicalOS heartbeat client: the frozen envelope and per-product bodies, the canonical runtime validator, derived instance identity, consent state with provenance, and an SSRF-isolated dual-send transport. Pseudonymous by design and honest abou

Readme

@mythicalos/telemetry

The shared heartbeat client for the mythicalOS product family: one frozen envelope, three per-product bodies, one runtime validator, one identity derivation, one consent model, one transport.

Why one package: the privacy rules must exist exactly once. Three hand-maintained copies of "counts and buckets only, nothing nameable" is three chances to leak, and a client and a collector that each keep their own validator will drift — a broader client gets its payloads rejected, a narrower one loses real data. Keeping the schema, the emitter and the validator together is what lets a single lockstep test span all three.

What is collected

The complete field list — every leaf, its producer, its value class and its temporal class — is in docs/FIELDS.md at the repository root. Nothing outside that list is sent, and the schema rejects undeclared fields structurally at every level.

The payload carries no names, paths, hostnames, emails, prompts, source, SQL, job names or database identifiers.

It is pseudonymous, not anonymous — and we say so

The instance id is stable across days, so the daily records form a longitudinal series, and a rare combination of version, OS/arch and metric mix can single out a small installation. Do not describe this payload as "anonymous" in UI copy, a README, or a privacy notice.

Scope

This package covers the heartbeat channel. A product may have other outbound channels with their own consent story. Do not write "telemetry carries no identifiers" over a product whose other channels say otherwise — scope the claim to usage telemetry and describe the rest separately.

Identity — derived, stateless, no shared secret

instance_secret  32 random bytes, hex. NEVER serialized on any HTTP route.
instance_id      uuidv4Format(sha256(instance_secret)[0..16])

Ingest authorization is the collector recomputing that derivation from the header secret and constant-time comparing it to the payload's claimed id. No registration, no stored key material, no first-writer race, and no shared or baked write key — one install cannot impersonate another, and there is no credential to leak. Do not introduce one.

⚠ The derivation hashes the UTF-8 bytes of the 64-character hex string, not the decoded 32 bytes. Changing that silently mints a new id for every existing install, orphaning its history and its delete capability. identity.test.ts pins known secret → id vectors for exactly this reason.

Destination-scoped copy identities

A copy destination gets its own secret and its own derived id. The central secret is never sent to a copy — an operator holding it could authenticate as that install at central and delete its data. Changing the copy URL rotates that identity and destroys the retired one; a secret is never carried to a new destination.

Consent

{ enabled: boolean, source: "unset" | "default-on" | "user", decided_at: string }

Only source: "unset" may be flipped on by a migration. source: "user" is never overwritten — by a migration or by anything else. source: "unset" never sends, whatever enabled says.

Provenance prevents future ambiguity; it cannot reconstruct what a pre-existing bare false meant. fromLegacyBoolean defaults to the conservative reading (treat it as a user opt-out) and the choice is overridable and documented, not silent.

Dual-send

Always central; optionally also an operator copy. A copy supplements central and never replaces it — copy-without-central is not a valid configuration and is rejected.

Delivery is not atomic and is deliberately not coupled: a flaky operator endpoint must not suppress central, and a central outage must not suppress the copy. Each (day, destination) keeps its own durable record — attempt count, last attempt, next attempt, terminal status — because a single per-day marker is wrong the moment there are two destinations: mark on first success and the copy never retries; leave it unmarked and central is re-sent on every copy retry.

Retries are bounded and jittered (a fleet-wide fixed retry after an outage is a thundering herd), single-flight per (day, destination), and carry an idempotency key so an operator collector can dedupe.

Opt-out is a hard fence. Disabling telemetry, or changing an endpoint or credential, cancels queued retries and purges pending state. The fence is per destination: changing only the copy URL does not cancel an unresolved central delivery whose consent and configuration are unchanged. Only a global opt-out fences both.

Partial state is exposed, not hidden — "central delivered, copy unresolved" is a real state and SendReport.partial reports it.

A copy endpoint that is configured but unusable does not suppress central. The two destinations are validated and fenced independently: a typo in the operator URL retires the copy and is reported on the emit result, while central's unresolved deliveries are untouched.

On-disk state, and what happens with two writers

Three documents under <stateRoot>/telemetry/: instance.json (the central identity), copy-identities.json (the destination-scoped copy identity), delivery.json (per-destination delivery records). Every write that replaces an existing document — delivery state, a rotation, the self-heal of an unreadable file — goes temp-then-rename, so a reader sees the old bytes or the new ones and never a torn mix. A first mint is not a replacement and does not use rename; see below.

A first mint is a claim, not a write. createFileExclusiveSync writes the content into a private same-directory temp and then link(2)s it into place, so the destination goes from absent to complete in one step; EEXIST means another writer got there first, and the loser adopts that identity instead of returning its own. This matters more than it looks: an id that exists only in one process's memory is data the installation can neither read nor delete, because the id is the capability a user exercises to ask for their data back. So a first mint resolves — claimed, or adopted — before centralIdentity() / copyIdentityFor() return anything. One exception, and it is deliberate: if the claim cannot be attempted at all (a full disk, a read-only mount, an I/O error), the store logs and returns a process-stable in-memory identity rather than throwing, and retries the write on the next call — telemetry never takes down the product it reports on, and the retry resolves rather than overwriting whatever landed meanwhile. A document that cannot be read is still replaced — nothing will ever release that name, and without the replace the install would never get a durable identity — but the replace is tied to the inode that was read, so a real document that turned up while the corrupt one was being judged is adopted instead of overwritten. Where the filesystem cannot hard-link, the claim degrades to an exclusive O_EXCL create — still exclusive, and inode-guarded so that losing the name mid-write reports the loss rather than a false success — and warns. The guard is a check and not a hold: it removes the window in which the destination is an empty file, not the instant between its last check and its return, which no filesystem primitive can remove.

Delivery state is never cached. It is re-read immediately before every mutation and the change is applied to what is actually on disk, so a second writer's records are not erased by a stale in-memory copy of the document — including across the HTTP round trip, which is a window no write may span. A reply that arrives after its delivery was fenced, or after a different delivery took the same key under a new generation, is discarded rather than applied.

The honest limits, because the shorter version would be a lie:

  • The read-modify-write is a single synchronous read → mutate → atomic write, and no more than that. The filesystem offers no compare-and-exchange, so two writers landing inside that window can still resolve to one, and two first sends of the same day can still both go out.
  • Rotation is a replace, not a claim, because superseding the stored identity is the whole point of rotateCentral(). Two processes rotating central at the same instant can still each mint, and the loser's secret will not be the one on disk. (Changing the copy destination is also a replacement, but it goes through the inode-tied path and keeps anything already holding that destination, so two processes moving to the same new endpoint converge on one secret.)
  • After a failed write, centralIdentity() can return a different identity later in the same process. An identity that never reached disk has no claim on the name, so the retry claims and — if another writer got there first — adopts what is actually stored rather than renaming over it. Read the identity per use rather than caching it across a process's lifetime, which the emitter already does.
  • The opt-out fence is process-local. It cancels in-flight requests and purges pending state in the process that observes it; it cannot reach into another process's open socket. Each process reads consent live and stops on its own next tick.

One writer per state directory remains the supported topology. What changed is that no path which merely reads now destroys an identity: it claims, adopts, or leaves it alone. Every write that replaces one is either a deliberate act — rotateCentral(), a copy destination change, clearCopyIdentity() retiring a removed endpoint — or the self-heal of a document that cannot be read, which is inode-tied so that anything real appearing meanwhile is adopted instead. What is left is the irreducible check-then-act instants above.

transport.record() returns a detached snapshot — mutating it changes nothing.

Operator-endpoint isolation

The copy destination is attacker-controlled input, fetched from inside the deployment. src/ssrf.ts enforces: https only (with an explicit opt-in for http to loopback), no redirect following, resolve-and-block of loopback / link-local / RFC1918 / CGNAT / ULA / IPv4-mapped / NAT64 / 6to4 / multicast / reserved space with every resolved address required to pass, a hard per-attempt deadline plus an overall fan-out budget, a bounded response read, bounded concurrency, and a circuit breaker.

Nothing the endpoint sends back is retained. The response body is drained under a bound and discarded; what is kept is the HTTP status and, for a transport-level refusal, a reason from a closed set this package defines. Earlier revisions kept a scrubbed excerpt as an "actionable error" and tried to filter key material out of it — but the endpoint already holds the write key, so it chooses the spelling, and three successive filters were each defeated by the next encoding (punctuation-separated, then alphanumeric-interleaved, then \uXXXX escapes). Any content filter that passes prose will pass some encoding of a secret, so the class was removed rather than filtered. A status code is still specific and still actionable, and it carries no attacker-chosen bytes into local state, a log, or a support screenshot.

The connection is pinned to the validated address. Validate-then-fetch leaves a DNS-rebinding window in which validation sees a public address and the connection resolves to 127.0.0.1; the socket layer is handed a resolver that can only return the address already validated, so there is no second resolution to race. SNI and certificate verification stay on the original hostname, so pinning costs nothing in TLS identity.

Disclosure

emitter.disclosure() returns the exact wire bytes per destination, as a collection — with dual-send the central and copy bodies differ (different instance_id), so a single response would be exact for at most one of them. It works while telemetry is off, which is the one moment someone most wants to look.

Usage

import {
  HeartbeatEmitter, IdentityStore, Transport,
  buildSagaMetrics, normalizeDeltas, applyDefaultOn, parseConsent,
} from "@mythicalos/telemetry";

const identity = new IdentityStore({ stateRoot });
const transport = new Transport({ stateRoot });

const emitter = new HeartbeatEmitter({
  product: "saga",
  version: VERSION,
  identity,
  transport,
  getConsent: () => readConsent(),          // read LIVE — a flip takes effect next tick
  getEndpoints: () => ({ centralUrl, copyUrl }),
  buildMetrics: (day) => buildSagaMetrics({ deltas, connections, uptimeSeconds }),
});

const result = await emitter.emit();        // { sent: false, reason: "opted_out" } when disabled

Products holding lifetime counters normalise first — the wire carries per-day deltas only:

const { deltas, snapshot } = normalizeDeltas(yesterdaySnapshot, currentLifetimeCounters);
persist(snapshot);

A counter that went backwards is a process restart, and normalizeDeltas emits the new value, never a negative. Only the producer can tell a restart from a genuine drop; the collector never can, which is why this belongs at the emitter.

Schema and the field-class manifest

  • schema/heartbeat.v1.json — the canonical schema, and the only one. additionalProperties: false at every level, including inside metrics. There is no second version and no dual ingest: a document either is this shape or is refused.
  • schema/field-classes.json — every leaf's value class and temporal class. cumulative is not a legal temporal value; there is no opaque-id value class.
  • scripts/check-field-classes.ts — the CI gate.

Two leaves were struck from the draft before the freeze, both for the same reason: no producer emitted them. skuld's events.deferrals was drafted against a counter whose code path had been removed, and saga's advisories.by_severity.critical names a severity the advisor does not have. Freezing either would have shipped a field that reads zero forever, which nobody can distinguish from "this install has none". Re-adding one is a new leaf in a new schema version, not an edit to this one — lockstep.test.ts pins both absences so the build fails rather than the review.

The check enforces SHAPE, not privacy. It cannot distinguish database_oid: integer from a legitimate count. Any new or reclassified leaf needs a named human privacy review recorded in the pull request. Do not describe this check as enforcing privacy.

Development

bun install
bun test          # includes lockstep.test.ts: JSON Schema ↔ validator ↔ real emitter output
bun run typecheck
bun run check:manifest

Licence

Apache-2.0. See LICENSE and NOTICE.