From 1798daf53c2ca098782414e4306af6e466095280 Mon Sep 17 00:00:00 2001 From: David Henning Date: Sat, 25 Jul 2026 20:57:06 +0200 Subject: [PATCH 1/2] feat: show relative time on timestamp claims and guard nil stdin stat Enrich the iat/exp/nbf annotations with the time remaining or elapsed instead of a bare flag: exp now reads "expires in 14m" / "expired 2h ago" and a future nbf "not yet valid, in 5m". An already-valid nbf stays bare. humanizeDuration renders the largest whole unit (s/m/h/d), truncating toward zero so output is deterministic. Annotations remain display-only and never affect verification or the exit code. Also guard readToken against a nil FileInfo when os.Stdin.Stat() fails (closed or detached stdin): it now falls through to the interactive prompt rather than dereferencing nil and panicking. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- README.md | 2 +- .../plans/2026-07-22-project-website.md | 1872 +++++++++++++++++ .../plans/2026-07-22-refined-website-copy.md | 247 +++ main.go | 7 +- main_test.go | 5 +- output.go | 33 +- output_test.go | 76 +- 8 files changed, 2222 insertions(+), 22 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-22-project-website.md create mode 100644 docs/superpowers/plans/2026-07-22-refined-website-copy.md diff --git a/AGENTS.md b/AGENTS.md index 8454ef2..312473f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,7 @@ Removing the fallback deleted the heuristics that existed only to decide it (`is ### `output.go` - Formatting, escaping, and colored printing - `printDecryptedPayload()` / `escapeTerminalText()` / `escapeFormattedJSONControls()` - Recursively decode nested JWTs/JWEs and pretty-print JSON objects or arrays; raw plaintext escapes C0 controls except newline/tab, DEL, C1 controls, invalid UTF-8 bytes, and targeted bidi controls, while formatted JSON sanitizes C1, DEL, and the same targeted bidi controls -- `formatTimestamps()` / `timestampStatus()` - Convert exact `iat`, `exp`, `nbf` Unix numeric values, including fractions, to RFC3339 strings (original value shown in parentheses); `exp` in the past is annotated `expired` and `nbf` in the future `not yet valid`. This is display-only and never affects verification or the exit code; `timeNow` is a package variable so the annotations are testable. The `--json` path skips this entirely and keeps raw numeric claims +- `formatTimestamps()` / `timestampStatus()` / `humanizeDuration()` - Convert exact `iat`, `exp`, `nbf` Unix numeric values, including fractions, to RFC3339 strings (original value shown in parentheses); `exp` is annotated with the time remaining or elapsed (`expires in 14m` / `expired 2h ago`) and a future `nbf` with `not yet valid, in 5m` (an already-valid `nbf` gets no note). `humanizeDuration` renders the largest whole unit (s/m/h/d), truncating toward zero for deterministic output. This is display-only and never affects verification or the exit code; `timeNow` is a package variable so the annotations are testable. The `--json` path skips this entirely and keeps raw numeric claims - `newFormatter()` - Creates a `go-prettyjson` formatter with the project color scheme - `printSection()` / `printSignature()` - Formatted output using `fatih/color` diff --git a/README.md b/README.md index e4ec899..9e5788d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A CLI tool that decodes and pretty-prints JSON Web Tokens (JWTs) and JSON Web En - `JWTD_KEY` environment variable for default key configuration - Syntax-highlighted JSON output with a consistent color scheme - Machine-readable output with `--json` for scripting and piping into tools like `jq` -- Automatic conversion of `iat`, `exp`, and `nbf` timestamps to human-readable RFC3339 dates, with `expired` / `not yet valid` annotations +- Automatic conversion of `iat`, `exp`, and `nbf` timestamps to human-readable RFC3339 dates, annotated with the time remaining or elapsed (`expires in 14m`, `expired 2h ago`, `not yet valid, in 5m`) - Accepts tokens as arguments, from stdin pipes, or via an interactive prompt - Colors auto-disable when output is not a TTY, or are controlled explicitly with `--color` - Shell completions (bash, zsh, fish) shipped in the Homebrew formula and the `.deb`/`.rpm` packages diff --git a/docs/superpowers/plans/2026-07-22-project-website.md b/docs/superpowers/plans/2026-07-22-project-website.md new file mode 100644 index 0000000..1060357 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-project-website.md @@ -0,0 +1,1872 @@ +# jwtd Project Website Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build and deploy a fast, accessible, progressively enhanced single-page website for jwtd at `https://jwtd.webcodr.io/`. + +**Architecture:** Serve a hand-built static document from `site/` with all essential content and anchor navigation in HTML, Tokyo Night presentation in one local stylesheet, and optional browser enhancements in one local script. A focused Go contract test locks down repository-owned website and GitHub Pages invariants, while Node's built-in test runner exercises the script's pure OS detection logic without adding npm or a frontend build step. + +**Tech Stack:** Semantic HTML5, CSS, browser JavaScript, Node.js 26.4.0 built-in test runner, Go 1.26 repository tests, GitHub Pages official actions. + +--- + +## File Map + +- Create: `site/index.html` - complete single-page content, metadata, semantic landmarks, no-JavaScript navigation, installation methods, usage documentation, and local asset references. +- Create: `site/styles.css` - responsive terminal-editorial layout, approved Tokyo Night tokens, CLI syntax colors, focus states, tab/mobile-menu enhancement states, and reduced-motion behavior. +- Create: `site/script.js` - pure OS detection plus accessible installation tabs, copy feedback, and mobile navigation enhancements. +- Create: `site/script.test.js` - dependency-free Node tests for OS detection, install-method mapping, fallback, and source-priority behavior. +- Create: `site/favicon.svg` - local, Tokyo Night-colored favicon with no external resource. +- Create: `site/CNAME` - exact custom domain declaration, `jwtd.webcodr.io`. +- Create: `site_test.go` - focused repository contract for the custom domain, canonical URL, core sections, local assets, CSP, Tokyo Night/CLI colors, Pages workflow, and pinned Node CI support. +- Create: `.github/workflows/pages.yml` - build-free Pages artifact upload and deployment with full-SHA official actions, least privilege, deployment environment, and concurrency. +- Modify: `.github/workflows/test.yml` - install a full-SHA-pinned Node 26.4.0 only in the test job, then run syntax and built-in tests; leave mise, release jobs, and all existing Go and release-package checks unchanged. + +Do not modify application source, release configuration, `.github/workflows/release.yml`, release packaging, README content, or any existing release behavior. DNS remains an external prerequisite: `jwtd.webcodr.io` must be a CNAME for `webcodr.github.io`. + +### Task 1: Add Tested Progressive Enhancements + +**Files:** +- Create: `site/script.test.js` +- Create: `site/script.js` + +- [ ] **Step 1: Write the failing OS detection tests** + +Create the site directory after verifying its parent: `ls . && mkdir site`. + +Expected: the repository root listing succeeds and the empty `site/` directory is created. + +Create `site/script.test.js`: + +```javascript +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { + detectOperatingSystem, + installMethodForOperatingSystem, +} = require("./script.js"); + +test("detectOperatingSystem classifies supported operating systems", () => { + const cases = [ + ["macOS", "", "", "macos"], + ["", "MacIntel", "", "macos"], + ["", "", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "macos"], + ["Windows", "", "", "windows"], + ["", "Win32", "", "windows"], + ["", "", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "windows"], + ["Linux", "", "", "linux"], + ["", "Linux x86_64", "", "linux"], + ["", "", "Mozilla/5.0 (X11; Linux x86_64)", "linux"], + ]; + + for (const [userAgentDataPlatform, platform, userAgent, expected] of cases) { + assert.equal( + detectOperatingSystem(userAgentDataPlatform, platform, userAgent), + expected, + ); + } +}); + +test("detectOperatingSystem returns unknown when no platform matches", () => { + assert.equal(detectOperatingSystem("", "", ""), "unknown"); + assert.equal(detectOperatingSystem("Plan 9", "Unknown", "custom-client"), "unknown"); +}); + +test("installMethodForOperatingSystem selects the approved default", () => { + assert.equal(installMethodForOperatingSystem("macos"), "homebrew"); + assert.equal(installMethodForOperatingSystem("windows"), "scoop"); + assert.equal(installMethodForOperatingSystem("linux"), "linux"); + assert.equal(installMethodForOperatingSystem("unknown"), "homebrew"); +}); + +test("detectOperatingSystem honors platform source priority", () => { + assert.equal( + detectOperatingSystem("Windows", "MacIntel", "Mozilla/5.0 (X11; Linux x86_64)"), + "windows", + ); + assert.equal( + detectOperatingSystem("", "MacIntel", "Mozilla/5.0 (Windows NT 10.0)"), + "macos", + ); +}); +``` + +- [ ] **Step 2: Run the tests and verify the module is missing** + +Run: `node --test site/script.test.js` + +Expected: FAIL with `Cannot find module './script.js'`. + +- [ ] **Step 3: Implement the pure detector and browser enhancements** + +Create `site/script.js`: + +```javascript +"use strict"; + +function classifyPlatform(value) { + const normalized = String(value || "").toLowerCase(); + + if (/mac|iphone|ipad|ipod/.test(normalized)) { + return "macos"; + } + if (/win/.test(normalized)) { + return "windows"; + } + if (/linux|x11/.test(normalized)) { + return "linux"; + } + return "unknown"; +} + +function detectOperatingSystem(userAgentDataPlatform, platform, userAgent) { + for (const candidate of [userAgentDataPlatform, platform, userAgent]) { + const detected = classifyPlatform(candidate); + if (detected !== "unknown") { + return detected; + } + } + return "unknown"; +} + +function installMethodForOperatingSystem(operatingSystem) { + if (operatingSystem === "windows") { + return "scoop"; + } + if (operatingSystem === "linux") { + return "linux"; + } + return "homebrew"; +} + +if (typeof module !== "undefined" && module.exports) { + module.exports = { detectOperatingSystem, installMethodForOperatingSystem }; +} + +if (typeof document !== "undefined") { + document.documentElement.classList.add("js"); + + const initialize = () => { + const tabs = Array.from(document.querySelectorAll('[role="tab"]')); + const panels = Array.from(document.querySelectorAll('[role="tabpanel"]')); + + const selectTab = (method, moveFocus = false) => { + for (const tab of tabs) { + const selected = tab.dataset.installMethod === method; + tab.setAttribute("aria-selected", String(selected)); + tab.tabIndex = selected ? 0 : -1; + if (selected && moveFocus) { + tab.focus(); + } + } + + for (const panel of panels) { + panel.hidden = panel.dataset.installPanel !== method; + } + }; + + if (tabs.length > 0 && panels.length > 0) { + let operatingSystem = "unknown"; + try { + operatingSystem = detectOperatingSystem( + navigator.userAgentData?.platform || "", + navigator.platform || "", + navigator.userAgent || "", + ); + } catch { + operatingSystem = "unknown"; + } + + selectTab(installMethodForOperatingSystem(operatingSystem)); + + tabs.forEach((tab, index) => { + tab.addEventListener("click", (event) => { + event.preventDefault(); + selectTab(tab.dataset.installMethod); + }); + + tab.addEventListener("keydown", (event) => { + let nextIndex; + if (event.key === "ArrowRight" || event.key === "ArrowDown") { + nextIndex = (index + 1) % tabs.length; + } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") { + nextIndex = (index - 1 + tabs.length) % tabs.length; + } else if (event.key === "Home") { + nextIndex = 0; + } else if (event.key === "End") { + nextIndex = tabs.length - 1; + } else { + return; + } + + event.preventDefault(); + selectTab(tabs[nextIndex].dataset.installMethod, true); + }); + }); + } + + const feedbackTimers = new WeakMap(); + for (const button of document.querySelectorAll("[data-copy-target]")) { + button.addEventListener("click", async () => { + const command = document.getElementById(button.dataset.copyTarget); + const feedback = button.parentElement.querySelector("[data-copy-feedback]"); + if (!command || !feedback) { + return; + } + + const previousTimer = feedbackTimers.get(button); + if (previousTimer) { + window.clearTimeout(previousTimer); + } + + try { + if (!navigator.clipboard?.writeText) { + throw new Error("Clipboard API unavailable"); + } + await navigator.clipboard.writeText(command.textContent.trim()); + feedback.textContent = "Copied."; + } catch { + feedback.textContent = "Select the command and copy it manually."; + } + + feedbackTimers.set( + button, + window.setTimeout(() => { + feedback.textContent = ""; + }, 3000), + ); + }); + } + + const navToggle = document.querySelector("[data-nav-toggle]"); + const navigation = document.getElementById("primary-navigation"); + if (navToggle && navigation) { + const closeNavigation = (restoreFocus = false) => { + navToggle.setAttribute("aria-expanded", "false"); + navigation.dataset.open = "false"; + if (restoreFocus) { + navToggle.focus(); + } + }; + + navToggle.addEventListener("click", () => { + const open = navToggle.getAttribute("aria-expanded") !== "true"; + navToggle.setAttribute("aria-expanded", String(open)); + navigation.dataset.open = String(open); + }); + + navigation.addEventListener("click", (event) => { + if (event.target.closest("a")) { + closeNavigation(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && navToggle.getAttribute("aria-expanded") === "true") { + closeNavigation(true); + } + }); + } + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initialize, { once: true }); + } else { + initialize(); + } +} +``` + +- [ ] **Step 4: Run JavaScript tests and syntax checking** + +Run: `node --test site/script.test.js && node --check site/script.js` + +Expected: four passing tests followed by a zero-exit syntax check with no output. + +- [ ] **Step 5: Commit the independently tested enhancement module** + +```bash +git add site/script.js site/script.test.js +git commit -m "feat: add website progressive enhancements" +``` + +### Task 2: Build the Static Page and Tokyo Night Presentation + +**Files:** +- Create: `site_test.go` +- Create: `site/index.html` +- Create: `site/styles.css` +- Create: `site/favicon.svg` +- Create: `site/CNAME` + +- [ ] **Step 1: Write the failing static-site contract test** + +Create `site_test.go`: + +```go +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func readWebsiteFile(t *testing.T, elements ...string) string { + t.Helper() + data, err := os.ReadFile(filepath.Join(elements...)) + if err != nil { + t.Fatalf("reading website file %s: %v", filepath.Join(elements...), err) + } + return string(data) +} + +func TestWebsiteContentContract(t *testing.T) { + if got, want := readWebsiteFile(t, "site", "CNAME"), "jwtd.webcodr.io\n"; got != want { + t.Fatalf("site/CNAME must be exactly %q, got %q", want, got) + } + + index := readWebsiteFile(t, "site", "index.html") + for label, required := range map[string]string{ + "canonical URL": ``, + "content security": `default-src 'none'`, + "skip link": `href="#main-content"`, + "header landmark": `