clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

/login/'s steps in panels, named "forge" in lowercase

cat committed 81106ae
The sign-in page's headings were sentence case and named the domain:
"Join src.rest", "Add a passkey", then "You are fox on src.rest" and "For
fox on src.rest" under it. Now each step is a ui.panel whose head strip
names it in lowercase chrome, with the forge's name: sign in, join forge,
add a passkey for fox, signed in as fox. The passkey step says who it is
for once, in its head strip. ssh commands on the page still show the
real host, to copy.
commit
81106ae94f4e75d932621ed91f53a611282893e3
parent
38873d01038d33e362cc24c6ba0c4dbf0733a257
author
cat <admin@dev.site>
ci
✓ passed in 88s, signed by cat
kind
refactor risk score 1.7 of 3, Jev's estimate
review
#21 A home page of profiles, recent and topics; motd for admins; /login/ in panels
ci attestation
forge-attestation 1
type ci
commit 81106ae94f4e75d932621ed91f53a611282893e3
tree 01dc277be89b666d7a4c6433d70f948a208c7428
script .forge/ci 2dbcda6e044dc4b05503abde8369e2528413d3c7
result pass
exit 0
started 2026-10-19T04:45:18Z
duration 88s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:3659b3efaa745bd918db98a6bc5bfa5dccc1ed2d750e3e415aa0364eae88ab36 283 lines
|pushed wip/i18n
|3181f9e running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|3181f9e pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|a1a3f5b running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|a1a3f5b fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|210c93e running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|210c93e pass in 0s, signed
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|warning: You appear to have cloned an empty repository.
|site home: deployed 29f863c -> /sites/ana/dotfiles/home/
|created ana/notes
|remote: review #1: /ana/notes/reviews/1/
|pushed ana/todo
|landing #1 A todo list: ana/todo -> main
|868b468 running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|868b468 pass in 0s, signed
|pushed main
|471d6d4 running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|471d6d4 pass in 0s, signed
|remote: review #7: /dami/greet/reviews/7/
|pushed dami/regions
|created dami/forge
|warning: You appear to have cloned an empty repository.
|site guide: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/
|site guide: deployed 4faed11 -> /sites/dami/forge/guide/
|(node:83757) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/dotfiles in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.869GhQ/wt/demo/root/.forge/www
|(node:83916) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/notes in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.869GhQ/wt/demo/root/.forge/www
|(node:84084) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/forge in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.869GhQ/wt/demo/root/.forge/www
|(node:84245) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.869GhQ/wt/demo/root/.forge/www
|(node:84999) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built - in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.869GhQ/wt/demo/root/.forge/www
|235 of 235 pages render the same without 11ty
|guide: 8 files, links ok
|ok

Signature: good, cat

5 files changed, +58 -26
Mlib/private-server.ts2+-
Msite/_includes/login.njk44+++++++++++++++---------------
Msite/assets/login.ts3+-
Mtest/scenarios/identity-sessions.sh2+-
Atest/scenarios/login-headings.sh33+++++++++++++++++++++++
M lib/private-server.ts +1 -1
M site/_includes/login.njk +22 -22
@@ -2,8 +2,10 @@
22 sign in with a passkey, add one from the link that ssh <host> passkey33 prints (/login/#enroll=<code>; login.ts does both), join from the link44 forge user invite prints (/login/#invite=<code>: an SSH key, then a5- passkey), and, signed in, sign out. Data: forge, p (title), user (or ''), csrf, signal (JSON or ''),6- next, ready (sign-in is set up: FORGE_BASE_URL), host. -#}5+ passkey), and, signed in, sign out. Each step is a panel named in6+ lowercase for the forge (FORGE_NAME), not its domain; ssh commands show7+ the real host, to copy. Data: forge, p (title), user (or ''), csrf,8+ signal (JSON or ''), next, ready (sign-in is set up: FORGE_BASE_URL). -#}79 {% extends "base.njk" %}810 {% import "ui.njk" as ui %}911 {% block scripts %}
@@ -14,44 +16,42 @@
1416 {% endblock %}1517 {% block content %}1618 {% css %}17-/* the sign-in page */19+/* the sign-in page: a panel for each step, the status and the help under them */1820 .login { max-width: 40em; }19-.login h1 { font-size: var(--text-xl); margin: 0 0 var(--space-3); }20-.login-status { min-height: 1.4em; }21+.login-status { min-height: 1.4em; margin: var(--space-3) 0 0; }2122 .login-status.bad { color: var(--fail); }2223 .login form { display: inline; }2324 .login .input { max-width: 36em; }2425 {% endcss %}25-<div class="panel login" id="login" data-next="{{ next }}"{% if signal %} data-signal="{{ signal }}"{% endif %}><div class="panel-body">26+<div class="login" id="login" data-next="{{ next }}"{% if signal %} data-signal="{{ signal }}"{% endif %}>2627 {% if user %}27-<h1>Signed in as {{ user }}</h1>28+{% set head %}<h2>signed in as <b>{{ user }}</b></h2>{% endset %}29+{% call ui.panel(head) %}<div class="panel-body">2830 <p>You see the pages of the private repos you can read: <a href="/private/">your private repos</a>.</p>2931 <form method="post" action="/login/logout"><input type="hidden" name="csrf" value="{{ csrf }}">{{ ui.button('sign out', false, 'id="signout"', 'submit') }}</form>32+</div>{% endcall %}3033 {% elif not ready %}31-<h1>Sign in</h1>34+{% call ui.panel('<h2>sign in</h2>') %}<div class="panel-body">3235 <p>Sign-in isn't set up here: the forge needs <code>FORGE_BASE_URL</code>, the origin passkeys belong to.</p>36+</div>{% endcall %}3337 {% else %}34-<div id="signin-part">35-<h1>Sign in</h1>38+{% call ui.panel('<h2>sign in</h2>', '', 'id="signin-part"') %}<div class="panel-body">3639 <p>Private repos show their pages to their readers. Sign in with a passkey you have added to see them.</p>3740 <p>{{ ui.button('sign in with a passkey', true, 'id="signin"') }}</p>38-</div>39-<div id="invite-part" hidden>40-<h1>Join {{ host }}</h1>41-<div id="invite-form">41+</div>{% endcall %}42+{% set head %}<h2>join {{ forge.config.name }}</h2>{% endset %}43+{% call ui.panel(head, '', 'id="invite-part" hidden') %}<div class="panel-body" id="invite-form">4244 <p>You are invited as <b id="invite-user">you</b><span id="invite-grant"></span>. Your SSH key is how you get in: paste the public half, the one line of a <code>.pub</code> file such as <code>~/.ssh/id_ed25519.pub</code>. None yet? <code>ssh-keygen -t ed25519</code> makes one.</p>4345 <p>{{ ui.textarea('ssh public key', 'ssh-ed25519 AAAA... you@laptop', 'id="invite-key" rows="3" spellcheck="false" autocomplete="off" autocapitalize="off" maxlength="8192"', true) }}</p>4446 <p>{{ ui.button('join', true, 'id="invite-join"') }}</p>45-</div>46-</div>47-<div id="enroll-part" hidden>48-<h1>Add a passkey</h1>49-<p id="joined" hidden>You are <b id="joined-user">in</b> on {{ host }}: <code>ssh {{ forge.config.ssh }} help</code> says what you can do over ssh. A passkey signs you in on the web as well, to see the pages of private repos you may read. Your browser, phone or password manager keeps it. Not now? <code>ssh {{ forge.config.ssh }} passkey</code> prints a link for one later.</p>50-<p id="enroll-for">For <b id="enroll-user">you</b> on {{ host }}. Your browser, phone or password manager keeps it, and signs you in with it from now on.</p>47+</div>{% endcall %}48+{% call ui.panel('<h2>add a passkey for <b id="enroll-user">you</b></h2>', '', 'id="enroll-part" hidden') %}<div class="panel-body">49+<p id="joined" hidden>You are in: <code>ssh {{ forge.config.ssh }} help</code> says what you can do over ssh. A passkey signs you in on the web as well, to see the pages of private repos you may read. Not now? <code>ssh {{ forge.config.ssh }} passkey</code> prints a link for one later.</p>50+<p>Your browser, phone or password manager keeps it, and signs you in with it from now on.</p>5151 <p>{{ ui.button('add a passkey', true, 'id="enroll"') }}</p>52-</div>52+</div>{% endcall %}5353 <p class="login-status" id="login-status" role="status"></p>5454 <p class="desc" id="login-help">No passkey yet, or lost them all? <code>ssh {{ forge.config.ssh }} passkey</code> prints a link that adds one: your ssh key is how you get in.</p>5555 {% endif %}56-</div></div>56+</div>5757 {% endblock %}
M site/assets/login.ts +1 -2
M test/scenarios/identity-sessions.sh +1 -1
A test/scenarios/login-headings.sh +33 -0
@@ -0,0 +1,33 @@
1+# /login/'s steps (sign in, join from an invite, add a passkey, signed in)2+# are panels whose head strips are lowercase chrome naming the forge by its3+# name, never its domain; ssh commands on the page show the real host, to4+# copy. The passkey step names who it is for once, in its head strip.5+. "$(dirname "$0")/../lib.sh"6+user dami7+export FORGE_BASE_URL=https://forge.test FORGE_SSH=git@forge.test8+private_server9+# heads file: the head strips of the page, one per line.10+heads() { grep -o '<header class="panel-head">.*</header>' "$1" | sed 's/<[^>]*>//g'; }11+http -s "$PRIV/login/" >"$T/page"12+eq "$(heads "$T/page")" "sign in13+join forge14+add a passkey for you"15+has "$T/page" '<section class="panel" id="invite-part" hidden><header class="panel-head"><h2>join forge</h2></header>'16+has "$T/page" '<section class="panel" id="enroll-part" hidden><header class="panel-head"><h2>add a passkey for <b id="enroll-user">you</b></h2></header>'17+has "$T/page" '<code>ssh git@forge\.test passkey</code>'18+lacks "$T/page" 'For <b|on forge\.test'19+heads "$T/page" | grep -q 'forge\.test' && fail "a heading names the domain"20+heads "$T/page" | grep -q '[A-Z]' && fail "a heading has capitals"21+22+code=$(ssh_as dami passkey | sed -n 's/^ .*#enroll=//p')23+ok node "$T_HOME/test/authenticator.ts" enroll "$PRIV" "$code" "$T/dami.key"24+c=$(node "$T_HOME/test/authenticator.ts" login "$PRIV" "$T/dami.key")25+http -s -H "Cookie: __Host-forge=$c" "$PRIV/login/" >"$T/in"26+eq "$(heads "$T/in")" "signed in as dami"27+28+# Without sign-in set up, still a panel.29+stop_servers30+unset FORGE_BASE_URL31+private_server32+http -s "$PRIV/login/" >"$T/off"33+eq "$(heads "$T/off")" "sign in"