<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>holger — artifacts at rest</title>
<style>
:root {
color-scheme: light;
--bg: #fefefe;
--bg-rgb: 254 254 254;
--surface: oklch(1 0 0);
--raised: #e5e5e3;
--line: oklch(0.885 0.005 75);
--line-strong: oklch(0.775 0.006 75);
--ink: oklch(0.225 0.008 70);
--ink-dim: oklch(0.435 0.008 70);
--ink-mute: oklch(0.565 0.007 70);
--pass: oklch(0.52 0.15 155);
--bad: oklch(0.53 0.20 25);
--sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
--mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, monospace;
--display: ui-serif, Georgia, "Iowan Old Style", "Palatino Linotype", Palatino, serif;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0; display: flex; min-height: 100dvh;
background: var(--bg); color: var(--ink);
font: 14.5px/1.55 var(--sans); -webkit-font-smoothing: antialiased;
}
:root { --backdrop-image: url("/holger.webp"); }
.art {
flex: 0 0 50%;
background-color: var(--bg);
background-image:
linear-gradient(to right, rgb(var(--bg-rgb) / 0) calc(100% - 90px), rgb(var(--bg-rgb) / 1)),
var(--backdrop-image);
background-size: auto, contain;
background-position: center, center;
background-repeat: no-repeat, no-repeat;
}
.side {
flex: 1 1 50%; min-width: 0; height: 100dvh; overflow: hidden;
display: flex; flex-direction: column; gap: 1.6rem;
padding: clamp(1.25rem, 3.2vw, 2.75rem);
}
.entry { flex: 0 0 auto; }
.tagline {
margin: 0 0 1.25rem; font-family: var(--display); font-size: 1.05rem;
color: var(--ink-dim);
}
h1 {
margin: 0 0 .35rem;
font-family: var(--display);
font-size: 2.1rem; font-weight: 500; letter-spacing: -.012em;
}
.by {
flex: 0 0 auto;
display: flex; align-items: center; flex-wrap: wrap; gap: .38rem;
margin: auto 0 0; font-family: var(--display); font-size: .95rem; color: var(--ink-dim);
}
.by strong { font-weight: 600; color: var(--ink); }
.by a { color: inherit; border-radius: 3px; }
button {
width: 100%; padding: .62rem .8rem; border-radius: 6px;
border: 1px solid transparent; background: var(--raised); color: var(--ink);
font: inherit; font-size: .92rem; font-weight: 550; cursor: pointer;
transition: background .15s cubic-bezier(.16,1,.3,1), border-color .15s cubic-bezier(.16,1,.3,1);
}
button:hover:not(:disabled) { background: var(--raised); border-color: var(--ink-mute); }
button:disabled { opacity: .45; cursor: not-allowed; }
button.primary { background: var(--ink); border-color: var(--ink); color: var(--surface); }
button.primary:hover:not(:disabled) { background: oklch(0 0 0); border-color: oklch(0 0 0); }
button.primary.working {
background: var(--surface); color: var(--ink); border-color: var(--ink);
}
button.working:disabled { opacity: 1; cursor: progress; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 5px; }
.msg { margin-top: .7rem; min-height: 1.2rem; font-size: .82rem; color: var(--ink-dim); }
.msg.err { color: var(--bad); font-family: var(--mono); overflow-wrap: anywhere; }
.msg.ok { color: var(--pass); }
.offer { margin: .9rem 0 0; color: var(--ink-dim); font-size: .86rem; }
details.reg {
margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--line);
font-size: .86rem;
}
details.reg[hidden] { display: none; }
details.reg summary {
cursor: pointer; color: var(--ink-dim); list-style: none;
}
details.reg summary::-webkit-details-marker { display: none; }
details.reg summary::before {
content: "\25B8"; display: inline-block; width: 1em;
transition: transform .15s cubic-bezier(.16,1,.3,1);
}
details.reg[open] summary::before { transform: rotate(90deg); }
details.reg summary:hover { color: var(--ink); }
.field { margin-top: .7rem; }
.field[hidden] { display: none; }
.field label { display: block; margin-bottom: .22rem; color: var(--ink-mute); font-size: .78rem; }
.field input, .field select {
width: 100%; padding: .45rem .55rem; border-radius: 5px;
border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink);
font: inherit; font-size: .86rem;
}
.field input { font-family: var(--mono); }
details.reg button { margin-top: .9rem; }
.offer a { color: var(--ink); }
.offer code { font-family: var(--mono); font-size: .92em; }
.browse { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: .55rem; }
.browse h2 {
flex: 0 0 auto; margin: .2rem 0 0; font-size: .72rem; font-weight: 600;
letter-spacing: .08em; text-transform: uppercase; color: #000;
}
#releases { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: .88rem; }
thead th {
position: sticky; top: 0; z-index: 1; background: var(--bg);
text-align: left; font-weight: 600; font-size: .72rem; letter-spacing: .06em;
text-transform: uppercase; color: var(--ink-mute);
padding: .3rem .5rem; border-bottom: 1px solid var(--line);
}
tbody td {
padding: .32rem .5rem; color: #000;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0;
}
tbody tr { border-radius: 5px; }
tbody tr:hover td { background: var(--raised); }
td.version, th.version { text-align: right; width: 1%; white-space: nowrap; max-width: none; }
.browse[hidden] { display: none; }
.empty { margin: .4rem .5rem; color: var(--ink-mute); font-size: .86rem; font-family: var(--sans); }
.roster { flex: 0 0 auto; max-height: 9.5rem; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
.roster li {
display: flex; align-items: baseline; gap: .5rem;
padding: .28rem .5rem; font-family: var(--mono); font-size: .88rem; color: #000;
}
.roster li:hover { background: var(--raised); border-radius: 5px; }
.roster .fmt { color: var(--ink-mute); font-size: .78rem; }
.roster .count { margin-left: auto; color: var(--ink-mute); font-size: .78rem; }
#repos { flex: 0 0 auto; }
@media (max-width: 720px) {
body { display: block; }
.art {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
opacity: .16; filter: saturate(.9);
}
.side {
position: relative; z-index: 1; width: 100%;
height: auto; min-height: 100dvh; overflow: visible;
padding: 1.25rem 1.1rem; gap: 1.2rem;
}
#releases { overflow: visible; }
button { border-color: var(--line-strong); }
thead th { background: var(--bg); }
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
</style>
</head>
<body>
<div class="art" aria-hidden="true"></div>
<div class="side">
<main class="entry">
<h1>Holger</h1>
<p class="tagline">Immutable artifact repository in pure Rust</p>
<button id="enter" class="primary" disabled> </button>
<div id="msg" class="msg" role="status" aria-live="polite">reading the server …</div>
<p class="offer" id="offer" hidden>Point a package manager at <code id="base-url"></code></p>
<p class="offer" id="register-offer" hidden>No account here? An administrator mints you a single-use invite link.</p>
<details class="reg" id="reg" hidden>
<summary id="reg-summary">Register the first passkey</summary>
<div class="field" id="token-row">
<label for="token">Break-glass admin token</label>
<input id="token" type="password" autocomplete="off" spellcheck="false"
placeholder="HOLGER_UI_ADMIN_TOKEN">
</div>
<div class="field">
<label for="user">User name</label>
<input id="user" type="text" autocomplete="username" spellcheck="false"
placeholder="lowercase, [a-z0-9._-]">
</div>
<div class="field">
<label for="reg-label">Key label</label>
<input id="reg-label" type="text" spellcheck="false" placeholder="yubikey">
</div>
<div class="field" id="role-row">
<label for="role">Role</label>
<select id="role">
<option value="viewer">viewer — read the console</option>
<option value="operator">operator — artifact work</option>
<option value="admin">admin — promotion, replication, passkeys</option>
</select>
</div>
<button id="register">Register this passkey</button>
</details>
</main>
<section class="browse" id="repos" hidden>
<h2 id="repos-heading">Public repositories</h2>
<ul class="roster" id="repos-list" aria-labelledby="repos-heading"></ul>
</section>
<section class="browse" id="browse" hidden>
<h2 id="releases-heading">Latest releases</h2>
<div id="releases">
<table aria-labelledby="releases-heading">
<thead>
<tr>
<th scope="col">Repository</th>
<th scope="col">Artifact</th>
<th scope="col" class="version">Version</th>
</tr>
</thead>
<tbody id="releases-body"></tbody>
</table>
<p class="empty" id="releases-empty" hidden></p>
</div>
</section>
<p class="by">
brought to you by
<a class="maker" href="https://www.vetra.se" target="_blank" rel="noopener noreferrer">
<img class="mark" src="/vetra.svg" width="96" height="18" alt="" aria-hidden="true">
<strong>Vetra AB</strong>
</a>
&
<img class="mark" src="/ignalina.png" width="18" height="18" alt="" aria-hidden="true">
<strong>Ignalina ApS</strong>
</p>
</div>
<script>
const msg = document.getElementById('msg');
function show(text, cls) { msg.textContent = text; msg.className = 'msg ' + (cls || ''); }
const INVITE = (function () {
try {
const params = new URLSearchParams(location.search);
const secret = params.get('invite') || '';
if (secret) {
params.delete('invite');
const rest = params.toString();
history.replaceState(null, '', location.pathname + (rest ? '?' + rest : ''));
}
return secret;
} catch (e) { return ''; }
})();
(function entry() {
const btn = document.getElementById('enter');
fetch('/-/front', { credentials: 'omit' })
.then(r => {
if (r.status === 401 || r.status === 403) {
throw new Error('holger-front-3: this server gates reads behind a credential, so its front page cannot name itself. Configure `require_auth_for_reads: false`, or read the console instead.');
}
if (r.status === 404) {
throw new Error('holger-front-4: this build of holger-server has no `/-/front` door. The page is newer than the server behind it.');
}
if (!r.ok) throw new Error('holger-front-4: /-/front answered ' + r.status);
return r.json();
})
.then(front => {
if (front.base_url) {
document.getElementById('base-url').textContent = front.base_url;
document.getElementById('offer').hidden = false;
}
if (front.login && front.login.label) {
btn.textContent = front.login.label;
btn.disabled = false;
btn.dataset.start = front.login.start || '';
if (front.login.door === 'here' && front.login.finish) {
btn.dataset.door = 'here';
btn.dataset.finish = front.login.finish;
btn.dataset.next = front.login.next || '/';
enrolment(front.login.enrol);
}
show('');
} else {
btn.textContent = 'No browser login';
show('holger-front-2: this server offers no browser login. Its doors are mTLS, OIDC and bearer — for cargo, pip, docker and the console, not for a tab.', 'err');
}
})
.catch(e => {
btn.textContent = 'Unavailable';
show(String(e && e.message || e), 'err');
});
btn.onclick = function () {
const start = btn.dataset.start;
if (!rooted(start)) return;
if (btn.dataset.door === 'here') { ceremony(btn); return; }
location.href = start;
};
})();
function rooted(start) {
if (typeof start !== 'string' || start === '') return false;
if (!start.startsWith('/') || start.startsWith('//') || start.startsWith('/\\')) return false;
if (/[\x00-\x20\x7f]/.test(start)) return false;
return true;
}
function b64urlToBuf(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const bin = atob(s), bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes.buffer;
}
function bufToB64url(buf) {
const bytes = new Uint8Array(buf);
let s = '';
for (const b of bytes) s += String.fromCharCode(b);
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function enrolment(enrol) {
const panel = document.getElementById('reg');
if (!panel || !enrol) return;
if (!rooted(enrol.start) || !rooted(enrol.finish) || !rooted(enrol.status)) return;
panel.dataset.start = enrol.start;
panel.dataset.finish = enrol.finish;
const offer = document.getElementById('register-offer');
if (offer) offer.hidden = false;
panel.hidden = false;
const button = document.getElementById('register');
if (button) button.onclick = register;
if (INVITE) {
panel.open = true;
const token = document.getElementById('token-row');
const role = document.getElementById('role-row');
if (token) token.hidden = true;
if (role) role.hidden = true;
const summary = document.getElementById('reg-summary');
if (summary) summary.textContent = 'Register your passkey';
show('you were invited — pick a user name and register your passkey', 'ok');
return;
}
fetch(enrol.status, { credentials: 'omit' })
.then(r => (r.ok ? r.json() : null))
.then(s => {
if (s && s.bootstrap_spent === true) {
panel.hidden = true;
if (offer) offer.hidden = false;
}
})
.catch(() => {});
}
async function register() {
const btn = document.getElementById('register');
const panel = document.getElementById('reg');
const start = panel.dataset.start, finish = panel.dataset.finish;
if (!rooted(start) || !rooted(finish)) return;
btn.disabled = true;
const token = document.getElementById('token').value.trim();
const label = document.getElementById('reg-label').value.trim() || 'passkey';
const user = document.getElementById('user').value.trim();
const role = document.getElementById('role').value;
const hdr = (!INVITE && token) ? { 'Authorization': 'Bearer ' + token } : {};
try {
if (!window.PublicKeyCredential || !navigator.credentials) {
throw new Error('this browser has no passkey support');
}
show('starting registration …');
const params = new URLSearchParams();
if (user) params.set('user', user);
if (INVITE) params.set('invite', INVITE);
const qs = params.toString();
const res = await fetch(start + (qs ? '?' + qs : ''), { headers: hdr, credentials: 'same-origin' });
if (!res.ok) throw new Error(await res.text() || 'requires a valid invite or admin token');
const begin = await res.json();
const pk = begin.publicKey;
pk.challenge = b64urlToBuf(pk.challenge);
pk.user.id = b64urlToBuf(pk.user.id);
pk.excludeCredentials = (pk.excludeCredentials || []).map(c => ({ ...c, id: b64urlToBuf(c.id) }));
const cred = await navigator.credentials.create({ publicKey: pk });
const body = { ceremony_id: begin.ceremony_id, label: label, user: user, role: role, response: {
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
attestationObject: bufToB64url(cred.response.attestationObject),
}};
if (INVITE) body.invite = INVITE;
const fin = await fetch(finish, {
method: 'POST', credentials: 'same-origin',
headers: { ...hdr, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (fin.ok) show('passkey registered — you can log in now.', 'ok');
else show(await fin.text() || 'registration failed', 'err');
} catch (e) { show(e.message || String(e), 'err'); }
finally { btn.disabled = false; }
}
async function ceremony(btn) {
const label = btn.textContent;
const start = btn.dataset.start, finish = btn.dataset.finish;
const next = rooted(btn.dataset.next) ? btn.dataset.next : '/';
if (!rooted(start) || !rooted(finish)) return;
btn.disabled = true;
btn.classList.add('working');
btn.textContent = 'Processing …';
try {
if (!window.PublicKeyCredential || !navigator.credentials) {
throw new Error('this browser has no passkey support');
}
show('waiting for your key …');
const res = await fetch(start, { credentials: 'same-origin' });
if (!res.ok) throw new Error(await res.text() || 'could not start');
const begin = await res.json();
const pk = begin.publicKey;
pk.challenge = b64urlToBuf(pk.challenge);
pk.allowCredentials = (pk.allowCredentials || []).map(c => ({ ...c, id: b64urlToBuf(c.id) }));
const cred = await navigator.credentials.get({ publicKey: pk });
const body = { ceremony_id: begin.ceremony_id, id: cred.id, response: {
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
authenticatorData: bufToB64url(cred.response.authenticatorData),
signature: bufToB64url(cred.response.signature),
}};
const fin = await fetch(finish, {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (fin.ok) { show('logged in — opening the console …', 'ok'); location.href = next; }
else show(await fin.text() || 'login was denied', 'err');
} catch (e) { show(e.message || String(e), 'err'); }
finally {
btn.disabled = false;
btn.classList.remove('working');
btn.textContent = label;
}
}
(function catalogue() {
const panel = document.getElementById('browse');
const body = document.getElementById('releases-body');
const empty = document.getElementById('releases-empty');
const repos = document.getElementById('repos');
const roster = document.getElementById('repos-list');
if (!panel || !body) return;
fetch('/-/releases', { credentials: 'omit' })
.then(r => (r.ok ? r.json() : null))
.then(doc => {
if (!doc || typeof doc !== 'object') return;
const names = doc.repositories;
if (roster && repos && Array.isArray(names) && names.length > 0) {
for (const r of names) {
const li = document.createElement('li');
const name = document.createElement('span');
name.textContent = String(r.name === undefined ? '' : r.name);
li.appendChild(name);
if (r.format) {
const fmt = document.createElement('span');
fmt.className = 'fmt';
fmt.textContent = String(r.format);
li.appendChild(fmt);
}
const count = document.createElement('span');
count.className = 'count';
const n = r.packages;
count.textContent = (n === 1 ? '1 package' : String(n === undefined ? 0 : n) + ' packages');
li.appendChild(count);
roster.appendChild(li);
}
repos.hidden = false;
}
const rows = doc.releases;
panel.hidden = false;
if (!Array.isArray(rows) || rows.length === 0) {
document.querySelector('#releases table').hidden = true;
empty.textContent = 'Nothing published yet.';
empty.hidden = false;
return;
}
for (const row of rows) {
const tr = document.createElement('tr');
for (const [key, cls] of [['repository', ''], ['artifact', ''], ['version', 'version']]) {
const td = document.createElement('td');
if (cls) td.className = cls;
td.textContent = String(row[key] === undefined ? '' : row[key]);
td.title = td.textContent;
tr.appendChild(td);
}
body.appendChild(tr);
}
})
.catch(() => {
});
})();
</script>
</body>
</html>