tools.book: serve a busy-only iCalendar feed on cal.lassul.us
For when.lassul.us (timeful) to show lass's availability without event
details, at https://cal.lassul.us/busy/<token>.ics.
With --busy-token-file, GET /busy/<token>.ics returns the busy time book
itself uses for free slots (CalDAV.busy plus held requests), so the feed
and the booking page agree on what blocks time: recurrences expanded,
all-day events from local midnight, TRANSPARENT and CANCELLED events
left out, TENTATIVE events (own ones and pending booking requests) kept
as busy, since book holds those slots too.
Intervals are merged so overlapping events cannot be counted, and each
VEVENT carries only DTSTART/DTEND in UTC, DTSTAMP (required by RFC
5545), SUMMARY "Busy", TRANSP OPAQUE and a UID hashed from its times.
The window runs from 7 days back to 90 days ahead: book's own 28 day
horizon is too short for polls planned weeks ahead, and the past week
keeps polls that are still open showing sensible data.
The feed is generated per request, so it is as fresh as the calendar;
an unreadable calendar gives 503, never an empty feed. Any other path
under /busy/ gives 404; the token is compared in constant time.
configs/book.nix: the token comes from the prompt-less vars generator
busy-feed and reaches book as a systemd credential, so it never lands in
the Nix store; nginx proxies all of cal.lassul.us/busy/ to book.
book.lassul.us returns 404 for /busy/ so the feed has a single URL.
configs.hermes-proxy: log in to assist.lassul.us via id.lassul.us
assist.lassul.us now sits behind oauth2-proxy on neoprism (provider
oidc, issuer https://id.lassul.us), wired in with nginx auth_request on
that vhost only. The nixpkgs nginx helper (oauth2-proxy.nginx) is not
used because it turns on services.nginx.recommendedProxySettings for
every vhost on the host; the same few locations are written out here.
Unauthenticated requests are redirected (307) to /oauth2/start with a
relative rd, which oauth2-proxy accepts without a whitelist-domain;
the provider button is skipped so it goes straight to Zitadel.
Access: the Zitadel project "assist" is declarative with a grant for
lass@lassul.us only, and its projectRoleCheck blocks everyone else at
the IdP. oauth2-proxy additionally allows only that address
(authenticated-emails-file, no email domains); its OIDC provider rejects
emails whose email_verified is false. The session user is the `sub`
claim. The client id/secret come from zitadel-reconcile's env file
(/var/lib/zitadel-apps/assist-web.env, OAUTH2_PROXY_CLIENT_ID/SECRET),
used as the unit's EnvironmentFile; the cookie secret from a new
neoprism vars generator (oauth2-proxy/cookie-secret, via LoadCredential).
Public without login, as Chrome fetches them without cookies to install
the page and verify the TWA: /.well-known/assetlinks.json,
/manifest.webmanifest, /icon-192.png, /icon-512.png.
After the login, nginx sets Authorization: Bearer <access token> towards
hermes-voice, from an include in /run written at boot by a root oneshot
(hermes-voice-auth-header, owned by nginx, mode 0600), so the token
never enters the store. For that the hermes-voice generator is now a
shared var (configs/hermes-voice-vars.nix, imported by coaxmetal and
neoprism); a shared generator gets a new value, so the token is rotated.
hermes-voice keeps checking the token, which still guards its retiolum
port; the gateway plugin keeps calling /approve on localhost with
API_SERVER_KEY.
The page no longer asks for or stores the token. Its fetches use
redirect: "manual", so an expired login (redirect to id.lassul.us,
which fetch cannot follow cross-origin) shows up as an opaqueredirect
and the page reloads into the login.
configs.hermes-proxy: log in to assist.lassul.us via id.lassul.us
assist.lassul.us now sits behind oauth2-proxy on neoprism (provider
oidc, issuer https://id.lassul.us), wired in with nginx auth_request on
that vhost only. The nixpkgs nginx helper (oauth2-proxy.nginx) is not
used because it turns on services.nginx.recommendedProxySettings for
every vhost on the host; the same few locations are written out here.
Unauthenticated requests are redirected (307) to /oauth2/start with a
relative rd, which oauth2-proxy accepts without a whitelist-domain;
the provider button is skipped so it goes straight to Zitadel.
Access: the Zitadel project "assist" is declarative with a grant for
lass@lassul.us only, and its projectRoleCheck blocks everyone else at
the IdP. oauth2-proxy additionally allows only that address
(authenticated-emails-file, no email domains); its OIDC provider rejects
emails whose email_verified is false. The session user is the `sub`
claim. The client id/secret come from zitadel-reconcile's env file
(/var/lib/zitadel-apps/assist-web.env, OAUTH2_PROXY_CLIENT_ID/SECRET),
used as the unit's EnvironmentFile; the cookie secret from a new
neoprism vars generator (oauth2-proxy/cookie-secret, via LoadCredential).
Public without login, as Chrome fetches them without cookies to install
the page and verify the TWA: /.well-known/assetlinks.json,
/manifest.webmanifest, /icon-192.png, /icon-512.png.
After the login, nginx sets Authorization: Bearer <access token> towards
hermes-voice, from an include in /run written at boot by a root oneshot
(hermes-voice-auth-header, owned by nginx, mode 0600), so the token
never enters the store. For that the hermes-voice generator is now a
shared var (configs/hermes-voice-vars.nix, imported by coaxmetal and
neoprism); a shared generator gets a new value, so the token is rotated.
hermes-voice keeps checking the token, which still guards its retiolum
port; the gateway plugin keeps calling /approve on localhost with
API_SERVER_KEY.
The page no longer asks for or stores the token. Its fetches use
redirect: "manual", so an expired login (redirect to id.lassul.us,
which fetch cannot follow cross-origin) shows up as an opaqueredirect
and the page reloads into the login.
configs.riot: leave ve-riot unmanaged by networkd
Since d9ed167 networkd installed the route to the container itself.
networkd adds it as soon as ve-riot gets carrier, before the container
start script runs `ip route add 10.233.1.2 dev ve-riot`, so the script
fails with "RTNETLINK answers: File exists" and container@riot ends up
in a restart loop. Before the reboot riot had never restarted since
d9ed167, so nobody noticed.
The script's route and address come from nixpkgs' nixos-containers
module and can't be turned off without patching nixpkgs (localAddress
also sets the container side), so the networkd route has to go. Now
ve-riot is Unmanaged=yes instead, and the start script owns its
address and route. Checked against systemd 261:
- An unmanaged link gets no configuration from networkd, so nothing is
in place before the script's `ip route add` and it succeeds on every
start.
- networkd only drops foreign routes for the link it is configuring
(link_drop_unmanaged_config, CONFIGURING state). When link_drop_routes
marks foreign routes it skips routes on other interfaces, "Otherwise,
routes assigned to unmanaged interfaces will be dropped". So a networkd
restart or reload no longer touches the script's route. That was the
bug d9ed167 fixed.
KeepConfiguration=static could not keep the script's route anyway: it
only protects foreign routes with protocol static, and `ip route add`
uses protocol boot. LinkLocalAddressing=no and KeepConfiguration=static
mean nothing on an unmanaged link, so they are gone, along with the
commented-out IPForward/IPMasquerade lines. Forwarding and NAT stay in
nftables and the sysctl. No other .network file on neoprism matches
ve-riot (99-ethernet-default-dhcp needs Kind=!*), but Unmanaged=yes
also keeps a later catch-all file from taking over ve-riot.