@miadi/node-service-kit
v0.1.1
Published
Runtime primitives for small Node services on phones and servers: atomic JSON with three-state reads, host identity and self-signed certs, display formatting, and tailnet peer calls. Zero dependencies, requireable and importable.
Readme
@miadi/node-service-kit
The runtime concerns a small Node service needs before it can do anything domain-specific — on a phone in Termux or on a server. Zero dependencies. Requireable from CJS and importable from ESM on Node ≥ 20.
Nothing here knows about episodes, compositions, Android paths, or any particular deployment. That is the line the package must not cross: the moment it does, the next service cannot absorb it.
const { writeJsonAtomic, readJsonSafe } = require("@miadi/node-service-kit/json");
import { localIp } from "@miadi/node-service-kit/host";Subpaths: ./json · ./host · ./format · ./tailnet (or the root barrel).
./json — writes that survive a power cut
writeJsonAtomic(path, value) writes through a temp file in the same directory and renames, so a reader never observes a partial document.
readJsonSafe(path) returns a discriminated union of the three states a caller must treat differently:
{ ok: true, data } | { ok: false, missing: true } | { ok: false, corrupt: true, raw }Absent is not corrupt. A file that will not parse holds a record — preserveCorruptFile(path, raw) copies it to a stamped sidecar and returns that path, so nothing overwrites it silently.
./host — who this machine is, and the cert it presents
localIp({ fallback, interfaces }) — first non-internal IPv4, or the fallback. Never throws; the interface table is injectable so the logic is testable.
ensureSelfSignedCert({ dir, commonName, altNames, regenerateOnIdentityChange, opensslPath }) — ensures a key/cert pair, regenerating when the stored subject no longer matches. Returns null rather than throwing when generation fails, so a service can fall back to plain HTTP instead of failing to boot. openssl is invoked with an argument array through execFileSync — no shell, so a hostile hostname cannot become a command.
./format — for services that render their own HTML
formatBytes · formatTimestampAbsolute · formatTimestampRelative (injectable now) · escapeHtml.
The escaper covers '. Six copies of it existed and they disagreed on exactly that character, which is the one that matters inside a single-quoted attribute.
./tailnet — peers, and the guard before you call one
resolveDeviceName({ tailscaleName, fileName, hostname }) — precedence with each candidate reduced to its first DNS label, lowercased. localhost is never an identity.
mergePeerSources(livePeers, configuredPeers, selfName) — a live entry wins over a configured one for the same device; self is dropped. Configured peers are needed because Termux has no tailscale CLI, so a phone cannot ask the daemon who is on the mesh.
isKnownPeerIp(ip, peers) — a well-formed IPv4 that is currently a peer. The peer list is a required argument; there is no ambient lookup, so the check cannot pass merely because a daemon happened to answer.
peerRequest(ip, port, path, opts) — HTTPS JSON to a peer. Certificate verification is waived because identity on the mesh comes from WireGuard keys, which makes isKnownPeerIp mandatory first and makes this unusable for any non-peer URL.
Provenance
./json is a port of web/lib/atomic-json.js from Gerico1007/gmtermux branch 141-r2-integration (authored by Gerico1007, 2026-07-16, issue #141), behaviour unchanged. ./tailnet is the pure half of web/lib/mesh-sync.js from the same branch. ./host and ./format consolidate copies that had drifted across five gmtermux services; where they disagreed, the disagreements became options rather than a winner — the certificate's alternate names, the no-address fallback, and whether an identity change forces regeneration.
Deliberately not absorbed: device-marker chrome, R2 transport, the workspace-suffix machinery, and the pending-drain — each carries one deployment's paths or one service's shape.
