A clean, ad-free sysadmin utility site hosted on Cloudflare Pages at sysadminstuff.net.
Pure HTML/CSS/JS frontend. Server-side logic runs in Cloudflare Pages Functions (JS, no Node deps).
Source lives in GitHub (CinderHillsDev/sysadminstuff) — Cloudflare Pages auto-deploys on every push to main.
This file is a historical build spec, kept roughly in sync with the live site so it stays useful as a reference — not a strict source of truth. When it and the code disagree, trust the code (and CLAUDE.md, which is the authoritative day-to-day guidance for working in this repo).
- Frontend: Static HTML/CSS/JS — no framework, no build step
- Shared pure logic:
lib/parse.mjs(validators/parsers/shaping used by Functions, importable by Node) andjs/core.js(same idea for the browser, alsorequire()'d bytests/smoke.mjs) — one implementation shared between the app and its tests - Backend: Cloudflare Pages Functions in
functions/api/— auto-routed by Cloudflare.functions/_middleware.jsgates every/api/*request: rejects non-GET/HEAD/OPTIONS, rejects oversized URLs, requires a same-originSec-Fetch-Site(blocks curl/scripts/other origins), and edge-caches successful GET responses per endpoint via the Cache API - DNS: Domain already on Cloudflare — just point Pages to sysadminstuff.net in dashboard
- Deploy: Git push to main → Cloudflare auto-deploys everything (static files + functions)
sysadminstuff/
├── CLAUDE.md
├── sysadminstuff-agent.md
├── README.md
├── PRIVACY.md
├── LICENSE # Apache-2.0 + Commons Clause (source-available, not OSI open source)
├── wrangler.toml
├── package.json
├── index.html
├── privacy.html
├── css/
│ ├── style.css
│ └── fonts/ # self-hosted IBM Plex Sans/Mono (.woff2, SIL OFL)
├── js/
│ ├── core.js # pure validators/parsers shared with tests — no DOM, no network
│ ├── app.js # tab routing, input handling, theme toggle, shared UI helpers
│ ├── dns.js # Lookup, Propagation, CAA, DNSSEC
│ ├── email.js # SPF, SPF Lookups, DMARC, DKIM, MX, Header Analyzer, Blacklist, Builder
│ ├── web.js # HTTP Headers, Redirects, TLS Grade
│ ├── network.js # ASN Lookup, Subnet Calculator, CIDR Tools, Geo, Reverse DNS
│ ├── cert.js # Decode PEM/CSR — 100% client-side
│ ├── whois.js # calls /api/whois
│ ├── myip.js # calls /api/myip
│ ├── m365.js # calls /api/tenant
│ ├── cloud.js # Cloud IP, Fingerprint, ARN
│ ├── utils.js # Base64, URL Encode, JWT Decoder
│ ├── tools.js # Hash, Password/Passphrase/UUID, Epoch, Cron, Chmod, JSON, Base Convert, Regex
│ └── wordlist.js # bundled EFF short wordlist (CC-BY) for the passphrase generator
├── lib/
│ └── parse.mjs # pure logic shared by Functions + tests/smoke.mjs (RDAP/ASN shaping, SSRF host-blocking, tenant parsing, SPF term walking…)
├── functions/
│ ├── _middleware.js # method/URL/origin gate + per-endpoint edge cache for all /api/*
│ └── api/
│ ├── whois.js # RDAP proxy, falls back to classic whois:43 for TLDs with no RDAP
│ ├── rbl.js # blacklist/RBL checks across 15 zones
│ ├── tls.js # TLS reachability probe via Workers connect() — grade A/F
│ ├── headers.js # fetch a URL server-side, return headers + redirect chain
│ ├── asn.js # ASN/IP lookup via bgpview.io
│ ├── tenant.js # M365/Entra tenant lookup via Microsoft public endpoints
│ ├── myip.js # reflects caller's IP/ASN/geo/edge info from request.cf — no upstream call
│ └── dns.js # server-side DoH fallback for when direct browser DoH is blocked
└── tests/
├── smoke.mjs # pure-logic unit tests, no network — safe for CI
├── e2e.mjs # live checks against a running `wrangler pages dev`
└── README.md
There is no functions/api/crtsh.js — a crt.sh-backed "Certificate Lookup" tool existed at one point but was removed after crt.sh became unreliable. js/cert.js now only holds the client-side PEM/CSR decoder.
DNS → Lookup | Propagation | CAA | DNSSEC
Email → SPF | SPF Lookups | DMARC | DKIM | MX | Header Analyzer | Blacklist | Builder
Web → HTTP Headers | Redirects | TLS Grade
Network → ASN Lookup | Subnet Calculator | CIDR Tools | Geo | Reverse DNS
Cert → (single tool — Decode PEM/CSR)
Whois → (single tool)
My IP → (single tool — reflects the caller's own address, no query needed)
M365 → (single tool)
Cloud → Cloud IP | Fingerprint | ARN
Password → (single tool — Password / Passphrase / UUID, all client-side)
Utils → Base64 | URL Encode | JWT Decoder | Hash | Epoch | Cron | Chmod | JSON | Base Convert | Regex
This order matches the primary tab nav in index.html and DEFAULT_SUBTAB in js/app.js. Tabs whose only subtab is main (Whois, My IP, M365, Password, and Cert) render a single panel with no secondary subtab nav. The shared input bar (js/app.js's SHARED_INPUT_TABS) is shown for DNS/Email/Web/Network/Whois/M365/Cloud and hidden for My IP/Password/Utils/Cert, since those are fully self-contained.
- Font: self-hosted IBM Plex Sans (UI — labels, headings, buttons, tabs) + IBM Plex Mono (data — table cells,
pre/code, IDs, hashes). Both loaded fromcss/fonts/*.woff2(SIL OFL) — no Google Fonts, no external requests at all - Three theme modes, cycled by the header toggle button: System (follows
prefers-color-scheme, live-updates) → Light → Dark. Persisted inlocalStorageundersas-theme. An inline script in<head>setsdata-themebefore first paint so there's no flash - Dark tokens (default): bg
#0b0e14, surface#131824, border#262c3a, text#e8ecf3, muted#8891a2, green#3fb968, yellow#d7a13a, red#f0605a, blue#5b9df0, accent#2dd4de - Light tokens: bg
#f5f6f8, surface#ffffff, border#d9dce2, text#1b1f27, muted#5b6270, green#1a7f4d, yellow#8a5d0a, red#c62828, blue#1868c9, accent#0f7a8a - Results rendered in monospace-for-data / sans-for-chrome, as above
- Responsive — works on mobile
- Copy buttons on data cells and result blocks (
⧉on cells,copyon cards)
<header>
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle color theme">
<span class="icon"></span><span class="label"></span>
</button>
<a href="/" class="home-link">
<h1><span class="logo-mark">>_</span>sysadminstuff.net</h1>
<p>Clean sysadmin tools. No ads, no fluff.</p>
</a>
</header>
<main>
<div class="input-bar" id="input-bar">
<input id="query" type="text" placeholder="domain, hostname, or IP address" autofocus />
<button id="run-btn">Run</button>
</div>
<nav class="tabs primary" id="primary-tabs">
<button class="tab active" data-tab="dns">DNS</button>
<button class="tab" data-tab="email">Email</button>
<button class="tab" data-tab="web">Web</button>
<button class="tab" data-tab="network">Network</button>
<button class="tab" data-tab="cert">Cert</button>
<button class="tab" data-tab="whois">Whois</button>
<button class="tab" data-tab="myip">My IP</button>
<button class="tab" data-tab="m365">M365</button>
<button class="tab" data-tab="cloud">Cloud</button>
<button class="tab" data-tab="password">Password</button>
<button class="tab" data-tab="utils">Utils</button>
</nav>
<!-- DNS — representative example of a multi-subtab tab -->
<div id="tab-dns" class="tab-panel active">
<nav class="tabs secondary">
<button class="subtab active" data-subtab="lookup">Lookup</button>
<button class="subtab" data-subtab="propagation">Propagation</button>
<button class="subtab" data-subtab="caa">CAA</button>
<button class="subtab" data-subtab="dnssec">DNSSEC</button>
</nav>
<div class="subpanel active" id="panel-dns-lookup"></div>
<div class="subpanel" id="panel-dns-propagation"></div>
<div class="subpanel" id="panel-dns-caa"></div>
<div class="subpanel" id="panel-dns-dnssec"></div>
</div>
<!-- Email / Web / Network / Cloud / Utils follow the same nav+subpanel
pattern — see Tab Structure above for the exact subtab list per tab. -->
<!-- Single-tool tabs have no secondary nav, just one subpanel -->
<div id="tab-cert" class="tab-panel">
<div class="subpanel active" id="panel-cert-main"></div>
</div>
<div id="tab-whois" class="tab-panel">
<div class="subpanel active" id="panel-whois-main"></div>
</div>
<div id="tab-myip" class="tab-panel">
<div class="subpanel active" id="panel-myip-main"></div>
</div>
<div id="tab-m365" class="tab-panel">
<div class="subpanel active" id="panel-m365-main"></div>
</div>
<div id="tab-password" class="tab-panel">
<div class="subpanel active" id="panel-password-main"></div>
</div>
</main>
<footer>
No logs. No tracking. No ads. Queries are not stored or recorded.
<a href="/privacy">Privacy</a> ·
Source available · <a href="https://github.com/CinderHillsDev/sysadminstuff">GitHub</a>
</footer>- Enter key triggers Run
- Switching tabs/subtabs re-runs if the query changed (
lastRanmemo per tab:sub key avoids redundant re-fetches) - Spinner/loading state per panel
- Copy icon on all result blocks
- URL params:
?q=example.com&tab=dns&sub=propagation— updated viahistory.pushState, restored on load and on popstate - Small tooltip (
data-tip) on each subtab explaining what it does - Utils/Password/My IP/Cert tabs ignore the shared input bar — each has its own inputs or needs none
Each file in functions/api/ exports an onRequest handler.
All functions return Content-Type: application/json, set Access-Control-Allow-Origin: *, and handle OPTIONS preflight. None of them ever log user input — see Privacy Requirements below. functions/_middleware.js runs in front of all of them.
- Detect IP vs domain from
?q=param - Domains: RDAP via
rdap.org, plus a small set of registry overrides for TLDs missing from the IANA bootstrap (RDAP_BOOTSTRAP_OVERRIDESinlib/parse.mjs) - If a TLD has no RDAP support at all, falls back to classic whois: ask
whois.iana.orgfor the TLD's referral server over TCP 43 (viacloudflare:socketsconnect()), then query that server and return the raw text - IPs: RDAP via
rdap.arin.net - SSRF guard (
isBlockedHost) rejects internal/reserved targets before any upstream call
Check 15 blacklists — reverse the IP's octets, prepend to each zone, do an A lookup via Cloudflare DoH. A resolving answer means listed. All 15 run in parallel.
Zones checked:
zen.spamhaus.org, bl.spamcop.net, b.barracudacentral.org, dnsbl.sorbs.net, spam.dnsbl.sorbs.net, cbl.abuseat.org, dnsbl-1.uceprotect.net, dnsbl-2.uceprotect.net, bl.mailspike.net, hostkarma.junkemailfilter.com, noptr.spamrats.com, spam.spamrats.com, dyna.spamrats.com, ix.dnsbl.manitu.net, db.wpbl.info.
Return: [{ list, listed: bool, response }]
Uses the Cloudflare Workers connect() TCP API to probe TLS reachability — not a full protocol/cipher matrix. Workers can't pin a specific TLS version per socket, so the check is:
- Attempt a handshake with
secureTransport: 'on'(Workers negotiate modern TLS 1.2/1.3 and validate the certificate chain) - Success → grade A ("modern TLS + valid cert"); the exact negotiated version/cipher isn't exposed by the platform, so it's reported as "TLS 1.2 / 1.3" together
- Failure → grade F, with a best-effort reason (certificate problem, timeout, other handshake failure)
- Legacy TLS 1.0/1.1 can't be probed this way (Workers won't negotiate down to them), so there is no B/C grade tier — it's binary
- Only ever probes port 443, to avoid becoming an arbitrary internal port scanner
- SSRF guard (
hostResolvesToBlocked) resolves DNS and rejects internal/reserved targets before connecting
- Accept
?url=param - Fetch target server-side, follow redirects manually hop by hop (capped at
MAX_HOPS) - Return:
[{ url, status, headers }]for each hop in the redirect chain - Final entry is the destination
- SSRF-guarded; the caller's IP is never forwarded upstream
- Accept
?q=param — ASN number (AS13335or13335) or IP address - ASN →
https://api.bgpview.io/asn/{asn} - IP →
https://api.bgpview.io/ip/{ip} - Return: ASN, name, country, description, announced prefixes (shaped by
lib/parse.mjs)
Microsoft 365 / Entra ID tenant lookup using public, unauthenticated Microsoft endpoints — no login, no API key.
- OpenID Connect metadata (
login.microsoftonline.com/{domain}/v2.0/.well-known/openid-configuration) → tenant GUID, region + sub-scope, cloud instance → classified into Commercial / GCC / GCC High / DoD GetUserRealm→ brand name, Managed vs. Federated (ADFS) identityGetFederationInformation(SOAP/Autodiscover) → other domains in the same tenant, best-effort
Reflects the caller's own public IP and whatever Cloudflare already knows about the request (CF-Connecting-IP, request.cf: ASN, org, city/region/country, colo, TLS version/cipher, HTTP protocol). No upstream call at all — everything comes from the edge for free, so it's never cached (the response is inherently per-request).
Server-side DNS-over-HTTPS fallback. The browser queries public DoH resolvers directly first (js/app.js's dohQuery); this endpoint exists only for networks that block direct DoH (corporate proxies, captive portals) so DNS-backed tools keep working via same-origin /api/dns. Not a user-facing "feature" on its own — it's plumbing behind DNS/CAA/DNSSEC/MX/SPF/RBL/etc.
Lookup subtab:
- Record type pills: A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, ALL
- Queries via
window.dohQuery— tries the public resolver directly from the browser first, falls back to/api/dnsif that's blocked - ALL: runs all non-PTR types in parallel, groups results by type
- PTR: auto-reverses an IP input (
1.2.3.4→4.3.2.1.in-addr.arpa) - Display: table per type — Name | TTL | Type | Data
Propagation subtab:
- Queries the same record across 3 resolvers in parallel, straight from the browser (no backend hop for the common case): Cloudflare, Google, DNS.SB
- Table: Resolver | Answer | TTL | Status
- All agree → green "propagated consistently" summary; any differ → grey/yellow
CAA subtab:
- Looks up the
CAArecord type; parses flags/tag/value - No CAA records → warns that any CA may issue for the domain
- Summarizes which CAs are authorized to issue (from
issue/issuewildtags)
DNSSEC subtab:
- Queries A (for the resolver's
ADflag), DS, and DNSKEY in parallel - Signed if the
ADflag is set or DS records exist in the parent zone - Shows AD flag / DS record count / DNSKEY count, plus the raw DS records if present
SPF subtab:
- TXT lookup on the bare domain via Cloudflare DoH; finds the
v=spf1record - Display raw record + parsed mechanisms table with plain-English explanations
- Qualifiers explained:
+pass,-fail,~softfail,?neutral
SPF Lookups subtab:
- Walks the full
include/redirecttree recursively and counts DNS lookups against RFC 7208 §4.6.4: at most 10 lookups, at most 2 "void" lookups (no records returned) — exceeding either is a PermError - Flags dead includes, cycles, and void
a/mxtargets in the tree existsmechanisms are counted but never resolved (an NXDOMAIN there is a normal macro-driven existence test, not an error)
DMARC subtab:
- TXT lookup on
_dmarc.{domain}; parses all tags (v, p, sp, pct, rua, ruf, adkim, aspf, fo, ri) - Policy badge: NONE (grey) / QUARANTINE (yellow) / REJECT (green)
- Warns if
p=none: monitoring mode only
DKIM subtab:
- Selector input; TXT lookup on
{selector}._domainkey.{domain} - Parses v, k, p (truncated + bit length), t; warns on
t=y(testing mode)
MX subtab:
- MX lookup; for each host resolves IPs and does a PTR lookup on each
- Table: Priority | MX Host | IP(s) | PTR Record | PTR Match ✓/✗
Header Analyzer subtab:
- Paste raw email headers; parses the
Received:chain in reverse with per-hop delays - Extracts SPF/DKIM/DMARC authentication results, X-Spam headers, Message-ID, Date
- Runs entirely client-side
Blacklist subtab:
- Input: IP or domain (resolves domain to IP first)
- Calls
/api/rbl?ip={ip}— table of all 15 zones, listed = red, clean = green
Builder subtab:
- Form-based SPF record builder (ip4 list, includes, a/mx flags,
~all/-all/?allpolicy) and DMARC record builder (policy, pct, alignment mode, rua) — generates the raw TXT record text, entirely client-side
HTTP Headers subtab:
- Calls
/api/headers?url={url}; displays all response headers - 7-point security-header scorecard:
Strict-Transport-Security,Content-Security-Policy,X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy,X-XSS-Protection
Redirects subtab:
- Uses the same
/api/headersendpoint; displays each hop (Step | URL | Status | Location) - 301 = green (permanent), 302 = yellow (temporary); warns past 3 hops
TLS Grade subtab:
- Calls
/api/tls?host={hostname} - Grade badge is A or F only (see the
functions/api/tls.jssection above for why) — a successful modern-TLS handshake with a valid cert is A, anything else is F
ASN Lookup subtab:
- Input: ASN (
AS13335or13335) or IP; calls/api/asn?q={query} - Displays ASN, org name, country, description, announced prefixes
Subnet Calculator subtab:
- Input: CIDR or IP + mask — entirely client-side, no API
- Network/broadcast address, mask + wildcard, usable host range, host count, IP class
CIDR Tools subtab:
- Is-IP-in-CIDR check, and splitting a CIDR into smaller subnets — entirely client-side
Geo subtab:
- Resolves hostname → IP via DoH first if needed
GET https://ipwho.is/{ip}(HTTPS + CORS, no API key, callable directly from the browser)- Displays IP, country + flag, region, city, ISP, org, ASN; notes geolocation is approximate
Reverse DNS subtab:
- PTR lookup via DoH (auto-reverses the IP), then forward-confirms by resolving the PTR hostname back and checking the match
- Paste a PEM certificate (
-----BEGIN CERTIFICATE-----) or CSR (-----BEGIN CERTIFICATE REQUEST-----) - Parsed entirely in the browser via
window.parseCertificate/window.parseCsr(implemented injs/core.js, shared withtests/smoke.mjs) — nothing is ever uploaded - Certificate view: status badge (VALID/EXPIRING SOON ≤30 days/EXPIRED), subject, issuer, SANs, serial, validity window, public key (algorithm + size), signature algorithm
- CSR view: subject, requested SANs, public key, signature algorithm
- This is the only thing left in the Cert tab. A
functions/api/crtsh.jsproxy backed a second "Lookup" subtab (certificate-transparency history from crt.sh) that was removed once crt.sh became unreliable — see the git history around the "Cert" tab if that's ever worth reviving with a different CT-log source.
- Detects IP vs domain, calls
/api/whois?q={query} - Domains: Registrar, status flags, Created, Updated, Expires, name servers (falls back to classic whois:43 text for TLDs with no RDAP — rendered as-is)
- IPs: Network name, CIDR, country, org, abuse contact
- Fallback link to
https://lookup.icann.orgif the result is empty
- Self-contained — needs no query, runs as soon as the tab opens
- Calls
/api/myip; big copyable IP up top, then network (ASN/org/location/postal/timezone/coordinates) and connection (Cloudflare colo, HTTP protocol, TLS version/cipher, user agent) tables - Cross-links into Whois / ASN / Geo / Reverse DNS / Blacklist, pre-filled with the detected IP
- Single query input (domain); calls
/api/tenant?domain={domain} - Shows tenant ID, brand name, Managed vs. Federated identity, cloud environment badge (Commercial/GCC/GCC High/DoD), and other domains in the tenant
Cloud IP subtab:
- Resolves input to an IP, rejects private/reserved addresses
- First tries an exact match against AWS's published ranges (
ip-ranges.amazonaws.com/ip-ranges.json) for precise region + service - Falls back to classifying the ASN org (via
ipwho.isconnection/ISP field) for non-AWS providers
Fingerprint subtab:
- Infers the hosting/CDN, email, and DNS provider behind a domain from its CNAME/MX/NS records — runs in the browser via DoH
ARN subtab:
- Breaks an AWS ARN into partition/service/region/account/resource — entirely client-side
- Base64: two textareas, encode/decode, auto-detects base64 input and defaults to decode
- URL Encode: two textareas,
encodeURIComponent/decodeURIComponent - JWT Decoder: auto-decodes on input (no button); header/payload/signature sections,
expshown as a human-readable date highlighted red if expired, notes that verifying the signature requires the secret/key
b64EncodeUtf8/b64DecodeUtf8/looksLikeBase64/decodeJwtParts live in js/core.js so the browser and tests/smoke.mjs share one implementation.
js/tools.js — Hash, Password/Passphrase/UUID, Epoch, Cron, Chmod, JSON, Base Convert, Regex (all client-side)
- Hash (registered under the Utils tab): live MD5, SHA-1/256/384/512 as you type, via
crypto.subtle(+ a bundled MD5 implementation, since SubtleCrypto doesn't support it) - Password / Passphrase / UUID (its own top-level Password tab): cryptographically random password generator (length, character-set toggles, "avoid ambiguous characters" I/l/1/i/O/0/o/|) with an entropy readout; passphrase generator drawing from the bundled EFF short wordlist (
js/wordlist.js, 1296 words) with separator/capitalize/number/symbol options and its own entropy readout; UUID v4 viacrypto.randomUUID() - Epoch: Unix timestamp ↔ human date, both directions
- Cron: explains a cron expression in plain English + next run times
- Chmod: octal ↔ symbolic permission calculator
- JSON: format / validate / minify
- Base Convert: hex/dec/oct/bin conversion
- Regex: test a pattern against sample text with match highlighting
All of crypto.getRandomValues/crypto.subtle/crypto.randomUUID — nothing here ever leaves the browser.
// Registry — modules call this instead of exposing window.runX directly
function registerRunner(tab, subtab, fn) { RUNNERS[`${tab}:${subtab}`] = fn; }
// URL param handling
function getParams() { ... } // parse ?q=&tab=&sub=
function setParams(q, tab, sub) { ... } // history.pushState
// UI helpers
function showLoading(panel, label) { ... }
function showError(panel, message) { ... }
function showResult(panel, html) { ... }
function card(title, bodyHtml, copyText) { ... }
function wireCopyButtons(root) { ... }
function copyToClipboard(text) { ... }
// Input helpers
function hostFromInput(str) { ... } // isIP/isDomain from core.js, else URL().hostname
// Shared DNS resolution — tries public DoH straight from the browser first
// (free, no Worker invocation), falls back to /api/dns only when that's blocked
async function dohQuery(name, type, resolver = 'cloudflare') { ... }
async function resolveToIP(hostname) { ... }
// Theme (system / light / dark), persisted to localStorage as 'sas-theme'
function applyTheme(mode) { ... }
function initTheme() { ... }
// Tab/subtab activation, driven by SHARED_INPUT_TABS / NO_QUERY_SUBTABS / DEFAULT_SUBTAB
function activate(tab, sub, opts) { ... }Pure validators (isIP, isIPv4, isIPv6, isDomain, isCIDR, isASN, isURL, normalizeURL) live in js/core.js, not app.js — that keeps a single implementation shared with tests/smoke.mjs.
- Network errors: "Could not reach [service]. Try again shortly."
- No records found: "No [type] records found for [domain]."
- Invalid input: validated before running, inline error in the panel
- Upstream rate limits: surfaced as a plain-English error from the relevant function (e.g. bgpview.io/ipwho.is/RDAP being slow or rate-limited); the API never bills you past Cloudflare's free-tier request cap
- All errors styled red (
showError), non-blocking — each panel fails independently, one broken API never breaks another tab
name = "sysadminstuff"
pages_build_output_dir = "."
compatibility_date = "2025-07-01"- Push repo to GitHub
- Cloudflare Pages dashboard → Create project → Connect GitHub repo
- Build settings: no build command, output directory
/ - Custom domain: add
sysadminstuff.net(already on Cloudflare — auto-configures DNS) - Functions deploy automatically from
functions/folder - Every push to main auto-deploys both static files and functions
- One paragraph description
- Live link: https://sysadminstuff.net
- Full feature list organized by tab
- APIs used with attribution: Cloudflare DoH, RDAP/ARIN, ipwho.is, bgpview.io, AWS published IP ranges, Microsoft public endpoints, DNS blacklists (Spamhaus et al) — no crt.sh
- Run locally:
npx wrangler pages dev .(runs static files + functions together) - Smoke/e2e test instructions
- Abuse protection & cost section (Workers free-tier cap, middleware defenses, optional Cloudflare rate-limiting rule)
- Cloudflare Pages deploy steps
- Project structure
- Contributing guide
- License: Apache-2.0 + Commons Clause (source-available, not MIT — see
LICENSE)
- No npm required for the frontend — plain JS files only;
package.jsonexists only foreslint(devDependency) and thenpm test/npm run lintscripts - For local dev with Functions:
npx wrangler pages dev . - Each
js/<tab>.jsmodule registers its run function viawindow.registerRunner(tab, subtab, fn)— it does not assignwindow.runXdirectly - Each
functions/api/*.jsmust exportonRequestas a named export - All functions must set
Access-Control-Allow-Origin: *and handle OPTIONS preflight - Test domains:
google.com,github.com,cloudflare.com - Test IP:
1.1.1.1 - Test ASN:
AS13335(Cloudflare) - Utils/Password/My IP tabs work with zero network — fully offline capable (My IP is the odd one: it calls
/api/myip, but that endpoint has no upstream dependency of its own) - Keep each js/ file and each function independently testable — add/update a
tests/smoke.mjscheck for any pure-logic change - The shared input bar is hidden for tabs in
NO_QUERY_SUBTABS/outsideSHARED_INPUT_TABS(Utils, Password, My IP, Cert)
This is a privacy-first tool. Users must be able to trust that nothing they type is stored or tracked.
Every function in functions/api/ must follow these rules:
- Never log user input. No
console.log,console.error, or any logging of:- Query parameters (
q,url,ip,host, or any user-supplied value) - Request URLs (which contain user input)
- Client IP addresses
- Any derived data from user input
- Query parameters (
- Logging is permitted only for internal errors with no user data attached:
// ALLOWED console.error('RDAP fetch failed:', response.status); // NEVER DO THIS console.error('RDAP fetch failed for query:', query); console.log('Request from IP:', request.headers.get('CF-Connecting-IP'));
- No third-party analytics, telemetry, or error-tracking services (no Sentry, no Datadog, nothing)
- Do not forward the user's IP to upstream APIs where avoidable. Do not include
X-Forwarded-Forheaders in upstream requests from functions.
- No Google Analytics, no Plausible, no Fathom, no tracking pixels, no beacon calls
- No cookies set anywhere — not even session cookies
- No localStorage or sessionStorage used for anything beyond UI state (selected theme, last selected tab)
- No external scripts or assets loaded at all — even the fonts (IBM Plex Sans/Mono) are self-hosted, so nothing leaks to a third party just from loading the page
These tools never send data anywhere and must display a visible note saying so:
- Cert → Decode PEM/CSR: certificate/CSR parsing never leaves the browser
- Password tab (Password / Passphrase / UUID): generated with a cryptographic RNG, never transmitted
- Network → Subnet Calculator, Network → CIDR Tools: calculated entirely in your browser
- Email → Header Analyzer, Email → Builder: parsed/built entirely in your browser
- Utils → Base64, URL Encode, JWT Decoder, Hash, Epoch, Cron, Chmod, JSON, Base Convert, Regex: encoded/decoded/computed entirely in your browser
Short privacy line in the footer, visible on every tab:
<footer>
No logs. No tracking. No ads.
Queries are not stored or recorded.
<a href="/privacy">Privacy</a> ·
Source available · <a href="https://github.com/CinderHillsDev/sysadminstuff">GitHub</a>
</footer>Plain English, no legal boilerplate. Covers:
- What data we collect: nothing. Queries are not logged or stored.
- Cloudflare: as the hosting provider, Cloudflare may collect standard infrastructure metrics (request counts, error rates) but we do not have access to query content and do not enable Cloudflare Web Analytics.
- Fonts: self-hosted, so no font request ever reaches a third party.
- Third-party APIs: queries are sent to public APIs (Cloudflare DoH, RDAP registries, ipwho.is, bgpview.io, AWS published IP ranges, Microsoft public endpoints) to perform lookups — this is inherent to how the tools work. These services have their own privacy policies. We do not forward the caller's IP to them where it can be avoided.
- No cookies, no localStorage for personal data, no tracking of any kind.
- Contact: link to GitHub issues for questions.
Dedicated Privacy section in README.md:
- "sysadminstuff.net does not log, store, or track queries."
- "No analytics. No cookies. No ads."
- "The fully client-side tools (Subnet Calculator, Cert Decode PEM/CSR, Password/Passphrase/UUID, and everything under Utils) run entirely in your browser and never transmit data."
- Link to PRIVACY.md for full details.