/*! trentpower.fr · /verify/verify.js · authored · signed via /integrity.json */ (function () { 'use strict'; var MAP = (typeof window !== 'undefined' && window.TP_VERIFICATION_MAP) || {}; function getLang() { var t = document.documentElement.lang || 'en'; return (typeof window.I18N === 'object' && window.I18N[t]) ? t : 'en'; } function t(key, fallback) { var lang = getLang(); var ref = (window.I18N && window.I18N[lang]) || {}; var parts = key.split('.'); for (var i = 0; i < parts.length; i++) { if (ref == null || typeof ref !== 'object') return fallback || ''; ref = ref[parts[i]]; } return (typeof ref === 'string') ? ref : (fallback || ''); } function normalisePath(raw) { if (!raw) return '/'; raw = String(raw).trim(); if (!raw) return '/'; if (raw.indexOf('//') !== -1) { try { var u = new URL(raw, location.origin); if (u.origin !== location.origin) return null; raw = u.pathname || '/'; } catch (_) { return null; } } if (raw.charAt(0) !== '/') raw = '/' + raw; raw = raw.replace(/\/index\.html$/, '/'); if (raw.indexOf('.') === -1 && raw.charAt(raw.length - 1) !== '/') raw += '/'; return raw; } function el(tag, attrs, text) { var n = document.createElement(tag); if (attrs) for (var k in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, k)) n.setAttribute(k, attrs[k]); } if (text != null) n.textContent = text; return n; } function safeHref(path) { if (!path) return '#'; if (path.charAt(0) === '/') return path; try { var u = new URL(path, location.origin); if (u.origin !== location.origin) return '#'; return u.pathname + (u.search || ''); } catch (_) { return '#'; } } function copy(text, button) { if (!navigator.clipboard) return; navigator.clipboard.writeText(text).then(function () { var prev = button.textContent; button.textContent = t('verify.action.copied', 'Copied'); button.setAttribute('data-state', 'copied'); setTimeout(function () { button.textContent = prev; button.removeAttribute('data-state'); }, 1400); }, function () { /* noop */ }); } // The "Verify locally" command blocks were removed by design: command- // line release verification belongs on /integrity/, where the full // signed-manifest workflow is already documented. /verify/ stays // purely a per-page record. The Connected records strip below points // visitors at /integrity.json and the release archive when they want // to drop into the command-line flow. // ─── Localised archive label ────────────────────────────── // Mirrors cite.template.js's buildArchiveLabel , derives a human // form from the trailing YYYY-MM in /integrity/releases/YYYY-MM/. var LOCALE_MONTHS_VERIFY = { en: ['January','February','March','April','May','June','July','August','September','October','November','December'], fr: ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'], it: ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre'], es: ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'], de: ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'] }; function archiveLabel(routePath) { if (!routePath) return ''; var m = /(\d{4})-(\d{2})\/?$/.exec(routePath); if (!m) return routePath; var year = parseInt(m[1], 10), month = parseInt(m[2], 10); if (!month || month < 1 || month > 12) return routePath; var lang = getLang(); var months = LOCALE_MONTHS_VERIFY[lang] || LOCALE_MONTHS_VERIFY.en; var monthName = months[month - 1]; if (lang === 'fr') return monthName.charAt(0).toUpperCase() + monthName.slice(1) + ' ' + year; if (lang === 'it') return monthName.charAt(0).toUpperCase() + monthName.slice(1) + ' ' + year; if (lang === 'es') return monthName.charAt(0).toUpperCase() + monthName.slice(1) + ' de ' + year; if (lang === 'de') return monthName + ' ' + year; return monthName + ' ' + year; } // ─── Section: Page record ────────────────────────────── // Editorial dossier, not a metadata table. Each group is a small // mono label + value: Citation (serif body, the emotional centre) // followed by Location, Evidence, Fingerprint, Archive in mono. // Hairlines separate groups, not rows. Actions sit at the bottom // as quiet utility-link mono. // ─── micro-grid row helper (phase 33 rewrite) ────────────────────────── // emits one row of the record-grid: //
//
LABEL
//
VALUE
//
// value can be a string, a single dom node, or an array of nodes/strings // (joined with
for the multi-line evidence row). all paths/hashes // wrap cleanly via the css `overflow-wrap: anywhere; word-break: break- // word` rules on `.record-grid code`. function buildRecordRow(labelKey, labelFallback, valueNode) { var row = el('div', { 'class': 'record-grid__row' }); row.appendChild(el('dt', null, t(labelKey, labelFallback))); var dd = el('dd'); if (typeof valueNode === 'string') { dd.textContent = valueNode; } else if (Array.isArray(valueNode)) { valueNode.forEach(function (n, i) { if (i > 0) dd.appendChild(el('br')); if (typeof n === 'string') dd.appendChild(document.createTextNode(n)); else dd.appendChild(n); }); } else if (valueNode) { dd.appendChild(valueNode); } row.appendChild(dd); return row; } function recordLink(href, label) { var a = el('a', { href: safeHref(href) }); a.appendChild(el('code', null, label)); return a; } function recordCode(text) { return el('code', null, text); } function buildThisPage(record) { // phase 33 · micro-grid record system. the card now reads as a // signed technical certificate: header (eyebrow + title + // status) above a definition-list of metadata rows (citation, // location, evidence, fingerprint, archive) below a hairline // actions strip. inspired by swiss archival records, museum // object labels, and printed release ledgers. // phase 51 · semantic upgrades —
, inner //
. local var names // ('section', 'header') keep the diff minimal. var section = el('article', { 'class': 'verify-card', 'aria-labelledby': 'verify-record-title', }); // ── header ── eyebrow + title + status line ───────────────────── var header = el('header', { 'class': 'verify-card__header' }); header.appendChild(el('p', { 'class': 'eyebrow' }, t('verify.thispage.kicker', 'Page record'))); header.appendChild(el('h2', { 'id': 'verify-record-title' }, record.title || '')); var statusBits = []; if (record.manifest_status === 'found') { statusBits.push(t('verify.thispage.status.short.signed', 'Signed')); } if (record.source) { statusBits.push(t('verify.thispage.status.short.source', 'Source')); } if (record.release) { statusBits.push(t('verify.thispage.status.short.archived', 'Archived')); } if (statusBits.length) { header.appendChild(el('p', { 'class': 'verify-status' }, statusBits.join(' · '))); } section.appendChild(header); // ── record-grid ── one row per field ──────────────────────────── var grid = el('dl', { 'class': 'record-grid' }); if (record.citation) { grid.appendChild(buildRecordRow( 'verify.thispage.group.citation', 'Citation', record.citation)); } // location · canonical url (with route as quiet second line on // sub-pages where the canonical and route differ). if (record.canonical || record.route || record.path) { var locParts = []; if (record.canonical) { locParts.push(recordLink(record.canonical, record.canonical)); } var routeStr = record.route || record.path; if (routeStr && routeStr !== '/') { locParts.push(recordCode(routeStr)); } if (locParts.length) { grid.appendChild(buildRecordRow( 'verify.thispage.group.location', 'Location', locParts)); } } // evidence · `/source/…` on line 1; meta // ("HTML · 26 KB · Validated 2026-05-11") as a `` block below. `.record-meta` carries the // smaller mono / fg3 styling so the supporting bytes/date sit // visibly under the primary file path. if (record.source || record.file_type || record.size_label || record.validated) { var evDd = el('div'); if (record.source) { evDd.appendChild(recordLink(record.reader || record.source, record.source)); } var metaBits = []; if (record.file_type) metaBits.push(record.file_type); if (record.size_label) metaBits.push(record.size_label); if (record.validated) { var validatedPrefix = t('verify.thispage.validated_prefix', 'Validated'); metaBits.push(validatedPrefix + ' ' + record.validated); } if (metaBits.length) { evDd.appendChild(el('span', { 'class': 'record-meta' }, metaBits.join(' · '))); } grid.appendChild(buildRecordRow( 'verify.thispage.group.evidence', 'Evidence', evDd)); } // fingerprint · `` with the full hash visible (record-grid // code wraps cleanly via `overflow-wrap: anywhere`). title + // aria-label still carry the full string for assistive tech and // for the copy-fingerprint action below. if (record.sha256 && record.sha256 !== '(missing)') { // phase 51 · with // for directional isolation. title + aria-label keep // the full hash exposed for assistive tech and copy-fingerprint. var fpCode = el('samp', { 'class': 'record-fingerprint page-hash', 'title': record.sha256, 'aria-label': record.sha256, }); fpCode.appendChild(el('bdi', {}, record.sha256)); grid.appendChild(buildRecordRow( 'verify.thispage.group.fingerprint', 'Fingerprint', fpCode)); } if (record.release) { grid.appendChild(buildRecordRow( 'verify.thispage.group.archive', 'Archive', recordLink(record.release, record.release))); } section.appendChild(grid); // ── actions strip ── copy citation / copy fingerprint / // view source code (reader) / raw source mirror. var hasCitation = !!record.citation; var hasFingerprint = !!(record.sha256 && record.sha256 !== '(missing)'); var hasReader = !!record.reader; var hasSource = !!record.source; if (hasCitation || hasFingerprint || hasReader || hasSource) { var actions = el('nav', { 'class': 'record-tools', 'aria-label': t('verify.thispage.actions_label', 'Page record actions'), }); if (hasCitation) { var copyCit = el('button', { type: 'button', 'class': 'record-inline-action' }, t('cite.overlay.action.copy_citation', 'Copy citation')); copyCit.addEventListener('click', function () { copy(record.citation, copyCit); }); actions.appendChild(copyCit); } if (hasFingerprint) { var copyFp = el('button', { type: 'button', 'class': 'record-inline-action' }, t('verify.action.copy_fingerprint', 'Copy fingerprint')); copyFp.addEventListener('click', function () { copy(record.sha256, copyFp); }); actions.appendChild(copyFp); } if (hasReader) { actions.appendChild(el('a', { 'href': safeHref(record.reader), 'class': 'record-inline-action' }, t('verify.action.view_source_code', 'View source code'))); } if (hasSource && !hasReader) { // fallback for routes without a reader (e.g. /source/ itself has no .txt mirror) actions.appendChild(el('a', { 'href': safeHref(record.source), 'class': 'record-inline-action' }, t('verify.action.open_source_mirror', 'Plain text'))); } section.appendChild(actions); } return section; } // ─── Section: Other records ──────────────────────────────── // editorial index of sibling routes. small h2 heading, two-column // grid of mono links. reads as a quiet appendix; the page-record // card stays the unambiguous primary object. current path renders // as a muted, non-linked span carrying aria-current="page". function buildChooser(currentPath) { var label = t('verify.chooser.heading', 'Other records'); var labelId = 'other-records-title'; var section = el('section', { 'class': 'verify-chooser other-records', 'aria-labelledby': labelId, }); section.appendChild(el('h2', { 'class': 'other-records-title', 'id': labelId, }, label)); var list = el('ul', { 'class': 'other-records-grid' }); var routes = [ { path: '/', key: 'verify.chooser.label.home', fb: 'Homepage' }, { path: '/privacy/', key: 'verify.chooser.label.privacy', fb: 'Privacy' }, { path: '/integrity/', key: 'verify.chooser.label.integrity', fb: 'Integrity' }, { path: '/verify/', key: 'verify.chooser.label.verify', fb: 'Verify' }, { path: '/source/', key: 'verify.chooser.label.source', fb: 'Source' }, { path: '/security/', key: 'verify.chooser.label.security', fb: 'Security' }, { path: '/integrity/releases/', key: 'verify.chooser.label.releases', fb: 'Releases' }, ]; routes.forEach(function (r) { var rlabel = t(r.key, r.fb); var li = el('li'); if (r.path === currentPath) { li.appendChild(el('span', { 'aria-current': 'page' }, rlabel)); } else { li.appendChild(el('a', { 'href': '/verify/?path=' + encodeURIComponent(r.path), }, rlabel)); } list.appendChild(li); }); section.appendChild(list); return section; } // Renderers // The h1 ("Verify this page") is i18n-bound and stays static. The // selected page is identified inside the page record card; the // hero never carries a contextual title. function renderSelected(record) { var root = document.getElementById('verify-root'); if (!root) return; while (root.firstChild) root.removeChild(root.firstChild); // phase 33 · buildThisPage now emits its own `.verify-card` // section (the micro-grid certificate). no outer wrapper // needed. chooser sits below as quiet secondary nav. root.appendChild(buildThisPage(record)); root.appendChild(buildChooser(record.path || record.route)); } // renderGeneral was used when /verify/ was visited with no path. The // page now defaults to the homepage record instead, so a fresh visit // to /verify/ feels useful rather than empty. init() handles the // fallback chain , if the homepage record were ever missing from the // map (it shouldn't be), renderUnknown takes over. function renderUnknown(rawPath) { var root = document.getElementById('verify-root'); if (!root) return; while (root.firstChild) root.removeChild(root.firstChild); // Calm notice for routes not in the verification map. var notice = el('section', { 'class': 'verify-unknown' }); notice.appendChild(el('h2', { 'class': 'verify-section-heading' }, t('verify.unknown.title', 'Route not in the verification map'))); if (rawPath) { notice.appendChild(el('p', { 'class': 'verify-unknown-path' }, rawPath)); } notice.appendChild(el('p', { 'class': 'verify-section-intro' }, t('verify.unknown.body', 'The published manifest only lists the canonical pages of trentpower.fr. Use the records below to inspect the manifest, signature and release archives directly.'))); // Compact actions strip , Source · Integrity manifest · Release archive. // Mirrors .verify-thispage-actions so the fallback never reads as broken. var actions = el('p', { 'class': 'verify-unknown-actions' }); function unknownLink(href, key, fallback) { var a = el('a', { 'class': 'verify-unknown-action', 'href': href }, t(key, fallback)); return a; } function unknownSep() { return el('span', { 'class': 'verify-unknown-actions-sep', 'aria-hidden': 'true' }, ' · '); } actions.appendChild(unknownLink('/source/', 'verify.unknown.action.source', 'Source')); actions.appendChild(unknownSep()); actions.appendChild(unknownLink('/integrity.json', 'verify.unknown.action.manifest', 'Integrity manifest')); actions.appendChild(unknownSep()); actions.appendChild(unknownLink('/integrity/releases/', 'verify.unknown.action.releases', 'Release archive')); notice.appendChild(actions); root.appendChild(notice); root.appendChild(buildChooser(null)); } function init() { var qs = new URLSearchParams(location.search); var raw = qs.get('path'); // Default to the homepage record when no ?path= is supplied , a // fresh visit to /verify/ should land on a real public record, not // an empty page. The homepage is the canonical default. var norm = raw ? normalisePath(raw) : '/'; if (norm === null) { renderUnknown(String(raw).slice(0, 80)); return; } var record = MAP[norm]; if (record) renderSelected(record); else renderUnknown(norm); } // Re-render when the language switcher fires (lang attribute changes). var observer = new MutationObserver(function (mutations) { for (var i = 0; i < mutations.length; i++) { if (mutations[i].attributeName === 'lang') { init(); break; } } }); observer.observe(document.documentElement, { attributes: true }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();