From 9a71cb117960539aca017b4f63336bcafbeae1c5 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Mon, 24 Aug 2026 23:12:38 -0400 Subject: [PATCH 01/19] I've been working on this for at least 3 months and it isn't even 50% done --- src/blog/0012/index.md | 261 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 src/blog/0012/index.md diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md new file mode 100644 index 0000000..8907a5b --- /dev/null +++ b/src/blog/0012/index.md @@ -0,0 +1,261 @@ +--- +postTitle: "One Must Imagine Sisyphus Happy" +excerpt: "20 minute adventure to fix a camera driver, In-and-Out. Surely..." +date: "2026-03-16" +author: Alex Westerman +slug: "0012" +keywords: "Nix, NixOS, software development, reproducible builds, devops, CI, CD, home lab, self hosting" +--- + +> Editor's Preface +> Some of the solutions to problems in this article could have been solved (faster) by me asking Chat-Gippity or equivalent AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published which will explain this decision later. +> +> So don't ask "wHy NoT uSe ClAuDe?" if you see something stupid. Embrace the madness with me and jump into the rabbit hole. + +Over a year ago, I noticed that the webcam on my 3D printer server suddenly stopped working. I remembered doing a ad-hoc package upgrade and thought maybe one of the libraries is broken. Naively, I checked the `apt` log (I was using Raspbian originally) and started to rollback packages I thought were the culprit. `ustreamer`? No change. `libcamera`? Crashes somehow, had to rollback further. Maybe it's `haproxy` (starting to grasp at straws)? No beans. Soon, I start rolling back Raspberry Pi kernel versions. No Luck. I check `dmesg` and my `config.txt`. Nothing out of the ordinary. Maybe I forgot to build a kernel module for the camera? It's included with the Raspberry Pi kernel, I see it get loaded. What if it's a `udev` rule I'm missing? Doesn't seem like it (after a brief skim of udev docs). I started to consider it might be a hardware issue, so I buy a new camera module and DSI cable. IT DOESN'T CHANGE ANYTHING. I even use a different Raspberry Pi 4 I had on my shelf. IT STILL DOESN'T CHANGE. + +At this point, I'm pissed. I spent two weeks at this point trying to rollback packages only to not fix the problem and potentially break something else without my knowledge. _I started to forget which things I rolled back_ and at some points it started to become a mess trying to figure out what changed with each rollback. Configuration changes to firmware were also _very easy to forget_, probably wasting more of my time even more with false leads. Then I started noticing _how much random bullshit_ was just in this Linux install and wondered how did it happen? Like any needle in a haystack, finding it becomes harder the more hay that has to be dug through. + +I decided to nuke the installation. But I wanted to avoid this problem of having to debug a break like this again. Just one command, maybe a reboot, and I could instantly restore my entire system before I did an upgrade. That would be nice. + +## The Journey Begins + +A few months, I was browsing my timeline on Twitter when I saw mentions of [NixOS](https://nixos.org) in a debate about good operating systems. I've lost the thread to time, but I looked into it and the concept interested me: Deployment/Infrastructure as Code but at a whole system level. I started reading documentation about the Nix language and package manager, and started toying around in a WSL environment to familiarize myself with the various driving concepts behind it. It took a few weeks but I soon had a WSL environment in pure NixOS where I could mess around with building system images. Then I started digging into how creating installer images work. Understanding the magic (and bullshit) really started to take place at this point. + +At a high-level, the building block of Nix/NixOS is the _**derivation**_, which defines how a package is built and installed, as well as how to run the exectuables in a hermetic* sandbox. Each derivation is constructed through the evaluation of _**Nix expressions**_, which is written using the _**Nix programming language**_. Evaluting a Nix expression will build the package _from source_** and save the resulting derivation into the _**Nix Store**_ along with metadata about the hermetic* build environment the derivation contents were compiled under. The contents of the Nix Store is _read-only_ even to root users. Installation of derivations do not copy files to directories like `/bin` or `/share`, rather symlinks are instead created to the derivation contents in the Nix Store. To further This paradigm is a bit tricky to get the hang of initially (I would know I spent a month trying to nail it down), so this example should help. + +> Editor's Note +> +> You might have noticed some asterisks on items in the previous section. Some clarifying notes before we move on: +> +> 1. With respect to hermetic sandboxes, I will elaborate on this in the final "Thoughts on Nix" section +> 2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the compiled blobs into the install path, but that defeats the key point of derivations. More on this in the last section as well. + +Suppose for some reason I don't have [`sed`](https://www.gnu.org/software/sed/) installed on my NixOS machine. In order to install it, I need to have a Nix expression defining how to make the derivation for `sed`. In [Nixpkgs](https://github.com/nixos/nixpkgs), the NixOS package repository and utility library, this is what the Nix expression defining the derivation for `sed` looks like (with some extra stuff to make it potentially a bit more clear): + +```nix +# Reference: https://github.com/NixOS/nixpkgs/blob/f20fda6d42187f1c54896b1356941472cc844972/pkgs/tools/text/gnused/default.nix +{ + lib, + stdenv, + fetchurl, + updateAutotoolsGnuConfigScriptsHook, + perl, +}: + +stdenv.mkDerivation rec { + pname = "gnused"; + version = "4.10"; + + # This could be changed to a different thing, provide some function returning + # a set of files to copy into the hermetic environment as sources before building. + # This one fetches the sed sources from the GNU mirrors. + src = fetchurl { + url = "mirror://gnu/sed/sed-${version}.tar.xz"; + sha256 = "sha256-uOchgrLslqNXTimYxHt6qmTMIM4ADY6awxPMB87PKMc="; + }; + + # What things this package provides. + outputs = [ + "out" + "info" + ]; + + nativeBuildInputs = [ + updateAutotoolsGnuConfigScriptsHook # This injects a step before configure to make the script "newer" + perl # we need perl available + ]; + + preConfigure = "patchShebangs ./build-aux/help2man"; + # Prevents attempts of running 'help2man' on cross-built binaries. + env = lib.optionalAttrs (stdenv.hostPlatform != stdenv.buildPlatform) { + PERL = "missing"; + }; + + # This is implicitly specified with derivations built in the + # Standard Environment (stdenv), but I add it here to better + # demonstrate how a derivation (at least those in/using Nixpkgs) works. + # + # There will be a bunch of `runHook` calls, which are a provided + # shell function in the effective script that is generated by Nix + # to build the derivation. These just make sure pre/post phase steps + # that are defined in derivations are executed. + configurePhase = '' + runHook preConfigure + ./bootstrap + ./configure + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + make + runHook postBuild + ''; + + checkPhase = '' + runHook preConfigure + make check + runHook preConfigure + ''; + + installPhase = '' + runHook preConfigure + make install + runHook preConfigure + ''; + + # This is metadata in Nixpkgs similar to pkginfo. + # Most derivations I write outside of nixpkgs don't use this + meta = { + homepage = "https://www.gnu.org/software/sed/"; + description = "GNU sed, a batch stream editor"; + + longDescription = '' + Sed (stream editor) isn't really a true text editor or text + processor. Instead, it is used to filter text, i.e., it takes + text input and performs some operation (or set of operations) on + it and outputs the modified text. Sed is typically used for + extracting part of a file using pattern matching or substituting + multiple occurrences of a string within a file. + ''; + + license = lib.licenses.gpl3Plus; + + platforms = lib.platforms.unix; + maintainers = with lib.maintainers; [ mic92 ]; + mainProgram = "sed"; + }; +} +``` + +Now evaluating this Nix expression will pull the sources for `sed`, compile it with `make` and run the provided tests, then install it in the Nix store. This can be seen with the following: + +```shell +$ nix eval nixpkgs#gnused +«derivation /nix/store/8a41q43mxgkvi3a1pihlp9xiakgqvggx-gnused-4.10.drv» +``` + +This store path is then able to be provided in other Nix expressions (both system derivations or other package derivations) or in temporary environments. For example, if I want a temporary environment which has `sed` available to use in the shell `PATH`, `nix-shell` can be used like such: + +```shell +# Just because sed is available in the local Nix store +# it isn't in this shell environment's path +$ sed --version +zsh: command not found: sed + +$ env | grep PATH +PATH=/bin;/usr/bin; + +# Let's add it +$ nix-shell -P gnused + +# This is now run in a new shell with updated PATH env +# Commands run in this shell will be indicated with % +% sed --version +sed (GNU sed) 4.10 +Copyright (C) 2026 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later . +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Written by Jay Fenlason, Tom Lord, Ken Pizzini, +Paolo Bonzini, Jim Meyering, and Assaf Gordon. + +This sed program was built without SELinux support. + +GNU sed home page: . +General help using GNU software: . +E-mail bug reports to: . + +# Check the updated path +$ env | grep PATH +PATH=/bin;/usr/bin;;/nix/store/8a41q43mxgkvi3a1pihlp9xiakgqvggx-gnused-4.10/bin +``` + +And that's the fundamentals of Nix (with of abridging). There is a lot I intentionally excluded in this description to focus more on my descent into homelabbing madness, but Nix solves some interesting problems related to software distribution and packaging that other existing solutions have had trouble with. The big one is [Dependency/DLL Hell](https://en.wikipedia.org/wiki/Dependency_hell) which was the most likely culprit behind my camera driver issues; Nix derivations being uniquely keyed in evaluation means that I can easily have true control over which depdencies and what version is used when building drivers or systems. Once I got the derivation for the camera streaming software working after a good chunk of debugging [Video4Linux configuration](*TODO*) and [Device Tree Overlays](*TODO*) (since default NixOS does not come with the Raspberry Pi OS defaults), it was extremely cathartic to see it simply _work_ after all that. But by that time, Black Friday just passed and I had a new toy shipped in the mail. + +## Network Attaching Your Mom (because she is fat) + +_TODO_ probably scratch this title + +I don't know what exactly compelled me to buy a NAS. Maybe it was the bajillion consumer hard drives I shucked out of portable HDDs/SSDs and questionable runtime stress I put them under. Maybe it was because I was learning so much sysadmin stuff at my job that I wanted a playground that wouldn't affect a million people. Either way, I got a $200 dollar discount on a 2-Bay UGREEN NAS and acquired two 14TB WD Red HDDs that black friday. As of writing, I made a good decision and am really happy with the purchase, but of course getting to this point was fairly rocky because I just _had_ to be quirky and use NixOS instead of my default UGREEN NAS software. Surely it can't be that bad. + +The first problem is to figure out how to get to a boot menu to even install Linux. After mashing Ctrl + F12 or some combination I get into the BIOS and select the USB drive. I boot into the NixOS live image. I start getting some disk UUIDs so I can write my partition layout with [disko](*TODO*). But suddenly, after about 3 minutes the NAS reboots. Just out of nowhere. Maybe the flash drive I had the installer on was bad. So I burned the live image again on a new flash drive. It reboots after 3 minutes again. I boot in but do nothing. Reboots in 3 minutes. I start questioning if UGREEN put some bizarre write-protection on their OS. I check the BIOS. It turns out there is a setting which acts as a [Watchdog Timer](*TODO*) and will restart the machine if it does not get a specific "message" from the running OS. I turn it off. I boot into the live image. I wait 3 minutes. Then I wait another 2 minutes. Then I wait another 5 minutes because I'm paraniod. After 3 days of on-off debugging. I have only figured out how to _run the NixOS live image on the NAS_, I haven't even installed a base system. + +This somehow didn't deter me from continuing the madness. + +Before I can install NixOS, I need to identify a partition scheme given this is a server-like Linux install focused on bulk storage. With this NAS, I have a 32GB internal eMMC (which currently holds bootloaders and the UGREEN OS), two HDD Bays which are each fileld with 14TB disks, and two M.2 NVMe slots on the inside of the left HDD bay which I inserted two 512GB sticks I scraped from old laptops. Just considering data resiliency itself is a fun little adventure into learning about [RAID](*TODO*) and filesystems supporting multi-disk arrays. Questions like "how fast do I need to write data", "how much data am I willing to lose", "what is the minimum storage I want available as actual storage" start to become relevant. I'm going to cut out a bunch of research and give highlights on my decisions for my partition setup: + +- All data/system partitions use [Btrfs](*TODO*). More advanced than [`ext4`](*TODO*) and supports configurable compression and defragmentation: useful for maximizing density of data storage. +- The eMMC uses a fairly simple partition layout (separapte `/boot` and `/`) seen in most simple Linux installs. +- I use [`mdadm`](*TODO*) for two separate RAID 0 (Mirror) groups + - The two 14TB HDDs are the "archival" RAID pool and intended to be read + slow write with the nature of that data. + - The two 512GB NVMe sticks are the "fast" RAID pool used for system swap, things that need fast writes (i.e. live databases, caches), and more frequently updated data. + +> Editor's Note +> +> Some quick things about the partition setup that I think are important to shout out: +> +> - Btrfs does support being the manager of the RAID pool, I couldn't figure out how to easily do it with disko. Plus `mdadm` is easier to hook up alerts to from my experience. +> - I could get away with the "fast" drives not being in a RAID 0 pool if there are backup jobs that regularly copy stuff to the archival RAID pool. But what's the fun in having only one RAID pool. + +After writing the partition configuraiton and executing the steps with disko, I now have empty partitions to install Nix into. Using disko provides the additional benefit that it becomes part of the system derivation and auto generates [systemd mounts](*TODO*) on system rebuilds. But the first install process a bit of "provisioning work" to deal with secrets. Why? [Because you don't want to store unencrypted secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). For this, I use [sops-nix](*TODO*), which allows associating a secrets YAML with a set of SSH Host keys to decrypt against. But those host keys _only exists after the system is installed and run for the first time_. So I have to have a provisional configuration which has the minimum information without encrypted secrets, get the public key, then add the encrypted secrets configuraiton and rebuild the system deivation. A small annoyance but afterward secrets can be declared securely for that machine easily, even across machines if configured as such. I'm not going to dive deeper into secrets management because that is a whole additional design discussion that I want to avoid in this already long post. + +So I have an empty NAS with NixOS installed and configured to be remoted into for management and remote system derivation updates. The next step is to make a [Network File Share](*TODO*). For this I chose [Samba](*TODO*) over NFS because of the need for Windows systems to be able to access it. So I start looking into how to configure Samba. I follow this [convenient NixOS wiki page for Samba](*TODO*), declare the bind mounts for the Samba shares I wanted, then wrote the Samba configuration. Then I rebuild the NAS system config and try to connect from my Windows machine. It fails immediately. And the worst part is that it's a simple "Something went wrong". I start fiddling with the Samba configuration. Maybe I messed up some security settings. I get a different error this time. I revert my changes. Still "something went wrong". I try to scrounge Event Viewer for more information. Nothing. I even try connecting with my iPhone because the Files app supports Samba. That fails with no indication either. Great. It's Christmas day and I am trying to figure out why my Samba configuration is broken. Then... I found it: + +_**TODO INSERT BSKY OR TWITTER POST**_ + +That is cursed. And also really annoying. But it works. And that's what matters. Samba having to translate how users are defined from Microsoft AD-like user system to some Linux user definition is black magic that I am relieved I do not need to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](<>) at the Btrfs layer. All of this backed up in a RAID 0 mirror that is acessible by any device on my network. + +But wait, I forgot to mention something that happened before I could even setup Samba. Yet another fun hiccup. + +## Once you've been shopping for PC parts, you'll want to beat Sam Altman to death + +***TODO also this title probably*** + +The UGREEN NAS model I purchased only came with 8GB of RAM. While you _could_ use swap space to act as extra RAM, it doesn't always scale that way especially if you have many processes with "hot" pages. I learned this by accident by trying to run NixOS rebuilds of _**TODO What was it again**_ on a 1GB Raspberry Pi 4 before I learned about Nix remote builders. + +_**TODO INSERT BSKY OR TWITTER POST**_ + +Luckily, UGREEN provided the option of upgrading the RAM. But because it was DDR5, I still needed to buy a fresh stick. I checked the specs and bought a 16GB stick of Corsair Vengance DDR5 rated for 4800 MT/s at 1.1V. The same operating conditions as the pre-installed 8GB stick. I install it. It goes well for about 5 hours, then I get a freeze. I eventually realize it's a hard freeze (not even a kernel panic) and did a power cycle. I think nothing of it untill 3 hours later, another freeze. I connect one of my Raspberry Pi HDMI displays and tape it to the side of the display just to monitor the `dmesg` log when it freezes. This time it survived until sometime while I was sleeping and now I don't have another data point to use for troubleshooting. I power cycle yet again, it freezes in 30 minutes! Each time I just let the system run idle with the exception of `htop` on the main TTY just to get a visual indicator when it freezes. And the freeze is abrupt and silent. No `dmesg`, systemd journal, or magic smoke* is indicative of what could be the issue. + +> Editor's Note: "magic smoke" refers to the mystical gas inside your electronic components that allow it to do things with electricity. Letting that smoke escape will destroy the functionality of the device. + +I start to panic internally. Surely it isn't something wrong with my drives right? I spent $300 per NAS drive and returning/exchanging them would be a massive hassle. I try running without the hard drives. Still get the freeze. I take out the SSD and run with no attached drives. Frozen after 2.5 hours. I start to take apart the NAS from it's chassis just to inspect the motherboard; maybe some magic smoke got trapped in the chassis? After an hour of non-destructive teardown, all the boards look as clean as a PCB should. I spend 2 hours putting it back together in a non-destructive way without a manual and am stumped. Then I think "surely it isn't the RAM?" So I do an experiment and use the pre-installed RAM stick. I turn on the NAS. + +A full day passes. Didn't freeze yet. I gained the courage to reinsert the SSDs and HDDs and power cycle the machine. + +Another full day passes. I check logs and nothing out of the ordinary like before. + +A second day passes. By this point I am convinced it was the RAM stick. + +When I purchased the RAM from Amazon, I spent $200 dollars USD. As of writing, the same stick is now *$400 USD*. I now have a $200 DDR5 paper-weight sitting with my stack of DDR3s and DDR4s, and the worst part is that I think it is actually functional. Since the BIOS of the NAS (and most BIOSes for that matter) don't provide options to change the parameters of the RAM, it could just be a configuration issue of putting a circle into a square hole: it fits, but there is a lot of gaps. Compared to the previous generation, DDR5 has some architectural changes such as moving power management to each DIMM which may be impacting something subtle that I'm just not aware about. I don't have the patience (or budget) to look into this further. So 8GB it is. + +> Editor's Note: the actual length of time to produce the freezes in this section, while random, are purely made up for narrative effect. + + +So now, I can surely start adding some things to my NAS beyond file storage. Surely that isn't another significant debate? + +## You ever what life was like without Docker? + +If you work in tech, you probably know about [Docker](***TODO***) and/or container-based deployments. Extend that with [Kubernetes](***TODO***), maybe some [GitOps](***TODO***) solution, and voila! Isolated, self-healing web application deployments that power most websites and apps. But remember: Docker and containers as a concept is fairly new in the history of computers, it was publicly released in _2013_. The original container engine, [Linux Containers](***TODO***) that Docker was built on, didn't even exist until 2008. In the grand scheme of web applications, + +## All My Homies Hate DNS + +## Thoughts on Nix + +- Learning Nix + -- How do packages work (derivation, store, realization) + -- Finagling with System Derivations +- Getting the printer functional-ish, then distracted +- Introducing the NAS + -- Bootstrapping Nix (Making a "hermetic" install) + -- Setting up services +- Networking Time + -- Bootstrapping yet another system + -- It's always DNS +- Nemesis + -- Managing kernel patches + -- Speeding up builds + -- Linux Ricing From ec01b1c95b61b31df8c6c1562933a049c235e895 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Mon, 24 Aug 2026 23:46:45 -0400 Subject: [PATCH 02/19] Markdown Lint --- src/blog/0012/index.md | 214 +++++++++++++++++++++++++++++++++-------- 1 file changed, 172 insertions(+), 42 deletions(-) diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index 8907a5b..c420aeb 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -8,30 +8,72 @@ keywords: "Nix, NixOS, software development, reproducible builds, devops, CI, CD --- > Editor's Preface -> Some of the solutions to problems in this article could have been solved (faster) by me asking Chat-Gippity or equivalent AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published which will explain this decision later. +> Some of the solutions to problems in this article could have been solved (faster) by me asking Chat-Gippity or equivalent +> AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage +> unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in +> solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published +> which will explain this decision later. > > So don't ask "wHy NoT uSe ClAuDe?" if you see something stupid. Embrace the madness with me and jump into the rabbit hole. - -Over a year ago, I noticed that the webcam on my 3D printer server suddenly stopped working. I remembered doing a ad-hoc package upgrade and thought maybe one of the libraries is broken. Naively, I checked the `apt` log (I was using Raspbian originally) and started to rollback packages I thought were the culprit. `ustreamer`? No change. `libcamera`? Crashes somehow, had to rollback further. Maybe it's `haproxy` (starting to grasp at straws)? No beans. Soon, I start rolling back Raspberry Pi kernel versions. No Luck. I check `dmesg` and my `config.txt`. Nothing out of the ordinary. Maybe I forgot to build a kernel module for the camera? It's included with the Raspberry Pi kernel, I see it get loaded. What if it's a `udev` rule I'm missing? Doesn't seem like it (after a brief skim of udev docs). I started to consider it might be a hardware issue, so I buy a new camera module and DSI cable. IT DOESN'T CHANGE ANYTHING. I even use a different Raspberry Pi 4 I had on my shelf. IT STILL DOESN'T CHANGE. - -At this point, I'm pissed. I spent two weeks at this point trying to rollback packages only to not fix the problem and potentially break something else without my knowledge. _I started to forget which things I rolled back_ and at some points it started to become a mess trying to figure out what changed with each rollback. Configuration changes to firmware were also _very easy to forget_, probably wasting more of my time even more with false leads. Then I started noticing _how much random bullshit_ was just in this Linux install and wondered how did it happen? Like any needle in a haystack, finding it becomes harder the more hay that has to be dug through. - -I decided to nuke the installation. But I wanted to avoid this problem of having to debug a break like this again. Just one command, maybe a reboot, and I could instantly restore my entire system before I did an upgrade. That would be nice. +> +> This is less of an "informative" post about something I learned and more of an insane journey with some commentary at +> the end. This is post is my equivalent of _[The Odyssey]_ with an additional mini YouTube video essay. This means I +> will embellish some points for narrative effect, but the core messages I want to convey remain intact. Enjoy this +> story almost 9 months in the making! + +Over a year ago, I noticed that the webcam on my 3D printer server suddenly stopped working. I remembered doing a ad-hoc +package upgrade and thought maybe one of the libraries is broken. Naively, I checked the `apt` log (I was using Raspbian +originally) and started to rollback packages I thought were the culprit. `ustreamer`? No change. `libcamera`? Crashes +somehow, had to rollback further. Maybe it's `haproxy` (starting to grasp at straws)? No beans. Soon, I start rolling +back Raspberry Pi kernel versions. No Luck. I check `dmesg` and my `config.txt`. Nothing out of the ordinary. Maybe I +forgot to build a kernel module for the camera? It's included with the Raspberry Pi kernel, I see it get loaded. What if +it's a `udev` rule I'm missing? Doesn't seem like it (after a brief skim of udev docs). I started to consider it might +be a hardware issue, so I buy a new camera module and DSI cable. IT DOESN'T CHANGE ANYTHING. I even use a different +Raspberry Pi 4 I had on my shelf. IT STILL DOESN'T CHANGE. + +At this point, I'm pissed. I spent two weeks at this point trying to rollback packages only to not fix the problem and +potentially break something else without my knowledge. _I started to forget which things I rolled back_ and at some +points it started to become a mess trying to figure out what changed with each rollback. Configuration changes to +firmware were also _very easy to forget_, probably wasting more of my time even more with false leads. Then I started +noticing _how much random bullshit_ was just in this Linux install and wondered how did it happen? Like any needle in a +haystack, finding it becomes harder the more hay that has to be dug through. + +I decided to nuke the installation. But I wanted to avoid this problem of having to debug a break like this again. Just +one command, maybe a reboot, and I could instantly restore my entire system before I did an upgrade. That would be nice. ## The Journey Begins -A few months, I was browsing my timeline on Twitter when I saw mentions of [NixOS](https://nixos.org) in a debate about good operating systems. I've lost the thread to time, but I looked into it and the concept interested me: Deployment/Infrastructure as Code but at a whole system level. I started reading documentation about the Nix language and package manager, and started toying around in a WSL environment to familiarize myself with the various driving concepts behind it. It took a few weeks but I soon had a WSL environment in pure NixOS where I could mess around with building system images. Then I started digging into how creating installer images work. Understanding the magic (and bullshit) really started to take place at this point. - -At a high-level, the building block of Nix/NixOS is the _**derivation**_, which defines how a package is built and installed, as well as how to run the exectuables in a hermetic* sandbox. Each derivation is constructed through the evaluation of _**Nix expressions**_, which is written using the _**Nix programming language**_. Evaluting a Nix expression will build the package _from source_** and save the resulting derivation into the _**Nix Store**_ along with metadata about the hermetic* build environment the derivation contents were compiled under. The contents of the Nix Store is _read-only_ even to root users. Installation of derivations do not copy files to directories like `/bin` or `/share`, rather symlinks are instead created to the derivation contents in the Nix Store. To further This paradigm is a bit tricky to get the hang of initially (I would know I spent a month trying to nail it down), so this example should help. +A few months, I was browsing my timeline on Twitter when I saw mentions of [NixOS](https://nixos.org) in a debate about +good operating systems. I've lost the thread to time, but I looked into it and the concept interested me: +Deployment/Infrastructure as Code but at a whole system level. I started reading documentation about the Nix language +and package manager, and started toying around in a WSL environment to familiarize myself with the various driving +concepts behind it. It took a few weeks but I soon had a WSL environment in pure NixOS where I could mess around with +building system images. Then I started digging into how creating installer images work. Understanding the magic (and +bullshit) really started to take place at this point. + +At a high-level, the building block of Nix/NixOS is the _**derivation**_, which defines how a package is built and +installed, as well as how to run the exectuables in a hermetic*sandbox. Each derivation is constructed through the +evaluation of ***Nix expressions***, which is written using the ***Nix programming language***. Evaluting a Nix +expression will build the package *from source*** and save the resulting derivation into the ***Nix Store*** along with +metadata about the hermetic* build environment the derivation contents were compiled under. The contents of the Nix Store +is _read-only_ even to root users*. Installation of derivations do not copy files to directories like `/bin` or `/share`, +rather symlinks are instead created to the derivation contents in the Nix Store. To further This paradigm is a bit tricky +to get the hang of initially (I would know I spent a month trying to nail it down), so this example should help. > Editor's Note > > You might have noticed some asterisks on items in the previous section. Some clarifying notes before we move on: > > 1. With respect to hermetic sandboxes, I will elaborate on this in the final "Thoughts on Nix" section -> 2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the compiled blobs into the install path, but that defeats the key point of derivations. More on this in the last section as well. +> 2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the +> compiled blobs into the install path, but that defeats the key point of derivations. More on this in the last section +> as well. +> 3. Technically, writing to the Nix store is done through the Nix Daemon _**TODO VERIFY**_ -Suppose for some reason I don't have [`sed`](https://www.gnu.org/software/sed/) installed on my NixOS machine. In order to install it, I need to have a Nix expression defining how to make the derivation for `sed`. In [Nixpkgs](https://github.com/nixos/nixpkgs), the NixOS package repository and utility library, this is what the Nix expression defining the derivation for `sed` looks like (with some extra stuff to make it potentially a bit more clear): +Suppose for some reason I don't have [`sed`](https://www.gnu.org/software/sed/) installed on my NixOS machine. In order +to install it, I need to have a Nix expression defining how to make the derivation for `sed`. In [Nixpkgs](https://github.com/nixos/nixpkgs), +the NixOS package repository and utility library, this is what the Nix expression defining the derivation for `sed` looks +like (with some extra stuff to make it potentially a bit more clear): ```nix # Reference: https://github.com/NixOS/nixpkgs/blob/f20fda6d42187f1c54896b1356941472cc844972/pkgs/tools/text/gnused/default.nix @@ -63,7 +105,7 @@ stdenv.mkDerivation rec { nativeBuildInputs = [ updateAutotoolsGnuConfigScriptsHook # This injects a step before configure to make the script "newer" - perl # we need perl available + perl # we need perl available ]; preConfigure = "patchShebangs ./build-aux/help2man"; @@ -81,7 +123,7 @@ stdenv.mkDerivation rec { # to build the derivation. These just make sure pre/post phase steps # that are defined in derivations are executed. configurePhase = '' - runHook preConfigure + runHook preConfigure ./bootstrap ./configure runHook postConfigure @@ -90,7 +132,7 @@ stdenv.mkDerivation rec { buildPhase = '' runHook preBuild make - runHook postBuild + runHook postBuild ''; checkPhase = '' @@ -129,14 +171,17 @@ stdenv.mkDerivation rec { } ``` -Now evaluating this Nix expression will pull the sources for `sed`, compile it with `make` and run the provided tests, then install it in the Nix store. This can be seen with the following: +Now evaluating this Nix expression will pull the sources for `sed`, compile it with `make` and run the provided tests, +then install it in the Nix store. This can be seen with the following: ```shell $ nix eval nixpkgs#gnused «derivation /nix/store/8a41q43mxgkvi3a1pihlp9xiakgqvggx-gnused-4.10.drv» ``` -This store path is then able to be provided in other Nix expressions (both system derivations or other package derivations) or in temporary environments. For example, if I want a temporary environment which has `sed` available to use in the shell `PATH`, `nix-shell` can be used like such: +This store path is then able to be provided in other Nix expressions (both system derivations or other package derivations) +or in temporary environments. For example, if I want a temporary environment which has `sed` available to use in the shell +`PATH`, `nix-shell` can be used like such: ```shell # Just because sed is available in the local Nix store @@ -173,56 +218,131 @@ $ env | grep PATH PATH=/bin;/usr/bin;;/nix/store/8a41q43mxgkvi3a1pihlp9xiakgqvggx-gnused-4.10/bin ``` -And that's the fundamentals of Nix (with of abridging). There is a lot I intentionally excluded in this description to focus more on my descent into homelabbing madness, but Nix solves some interesting problems related to software distribution and packaging that other existing solutions have had trouble with. The big one is [Dependency/DLL Hell](https://en.wikipedia.org/wiki/Dependency_hell) which was the most likely culprit behind my camera driver issues; Nix derivations being uniquely keyed in evaluation means that I can easily have true control over which depdencies and what version is used when building drivers or systems. Once I got the derivation for the camera streaming software working after a good chunk of debugging [Video4Linux configuration](*TODO*) and [Device Tree Overlays](*TODO*) (since default NixOS does not come with the Raspberry Pi OS defaults), it was extremely cathartic to see it simply _work_ after all that. But by that time, Black Friday just passed and I had a new toy shipped in the mail. +And that's the fundamentals of Nix (with of abridging). There is a lot I intentionally excluded in this description to +focus more on my descent into homelabbing madness, but Nix solves some interesting problems related to software +distribution and packaging that other existing solutions have had trouble with. The big one is [Dependency/DLL Hell](https://en.wikipedia.org/wiki/Dependency_hell) +which was the most likely culprit behind my camera driver issues; Nix derivations being uniquely keyed in evaluation means +that I can easily have true control over which depdencies and what version is used when building drivers or systems. Once +I got the derivation for the camera streaming software working after a good chunk of debugging [Video4Linux configuration](*TODO*) +and [Device Tree Overlays](*TODO*) (since default NixOS does not come with the Raspberry Pi OS defaults), it was extremely +cathartic to see it simply _work_ after all that. But by that time, Black Friday just passed and I had a new toy shipped +in the mail. ## Network Attaching Your Mom (because she is fat) _TODO_ probably scratch this title -I don't know what exactly compelled me to buy a NAS. Maybe it was the bajillion consumer hard drives I shucked out of portable HDDs/SSDs and questionable runtime stress I put them under. Maybe it was because I was learning so much sysadmin stuff at my job that I wanted a playground that wouldn't affect a million people. Either way, I got a $200 dollar discount on a 2-Bay UGREEN NAS and acquired two 14TB WD Red HDDs that black friday. As of writing, I made a good decision and am really happy with the purchase, but of course getting to this point was fairly rocky because I just _had_ to be quirky and use NixOS instead of my default UGREEN NAS software. Surely it can't be that bad. - -The first problem is to figure out how to get to a boot menu to even install Linux. After mashing Ctrl + F12 or some combination I get into the BIOS and select the USB drive. I boot into the NixOS live image. I start getting some disk UUIDs so I can write my partition layout with [disko](*TODO*). But suddenly, after about 3 minutes the NAS reboots. Just out of nowhere. Maybe the flash drive I had the installer on was bad. So I burned the live image again on a new flash drive. It reboots after 3 minutes again. I boot in but do nothing. Reboots in 3 minutes. I start questioning if UGREEN put some bizarre write-protection on their OS. I check the BIOS. It turns out there is a setting which acts as a [Watchdog Timer](*TODO*) and will restart the machine if it does not get a specific "message" from the running OS. I turn it off. I boot into the live image. I wait 3 minutes. Then I wait another 2 minutes. Then I wait another 5 minutes because I'm paraniod. After 3 days of on-off debugging. I have only figured out how to _run the NixOS live image on the NAS_, I haven't even installed a base system. +I don't know what exactly compelled me to buy a NAS. Maybe it was the bajillion consumer hard drives I shucked out of +portable HDDs/SSDs and questionable runtime stress I put them under. Maybe it was because I was learning so much sysadmin +stuff at my job that I wanted a playground that wouldn't affect a million people. Either way, I got a $200 dollar discount +on a 2-Bay UGREEN NAS and acquired two 14TB WD Red HDDs that black friday. As of writing, I made a good decision and am +really happy with the purchase, but of course getting to this point was fairly rocky because I just _had_ to be quirky +and use NixOS instead of my default UGREEN NAS software. Surely it can't be that bad. + +The first problem is to figure out how to get to a boot menu to even install Linux. After mashing Ctrl + F12 +or some combination I get into the BIOS and select the USB drive. I boot into the NixOS live image. I start getting some +disk UUIDs so I can write my partition layout with [disko](*TODO*). But suddenly, after about 3 minutes the NAS reboots. +Just out of nowhere. Maybe the flash drive I had the installer on was bad. So I burned the live image again on a new +flash drive. It reboots after 3 minutes again. I boot in but do nothing. Reboots in 3 minutes. I start questioning if +UGREEN put some bizarre write-protection on their OS. I check the BIOS. It turns out there is a setting which acts as a +[Watchdog Timer](*TODO*) and will restart the machine if it does not get a specific "message" from the running OS. I +turn it off. I boot into the live image. I wait 3 minutes. Then I wait another 2 minutes. Then I wait another 5 minutes +because I'm paraniod. After 3 days of on-off debugging. I have only figured out how to _run the NixOS live image on the +NAS_, I haven't even installed a base system. This somehow didn't deter me from continuing the madness. -Before I can install NixOS, I need to identify a partition scheme given this is a server-like Linux install focused on bulk storage. With this NAS, I have a 32GB internal eMMC (which currently holds bootloaders and the UGREEN OS), two HDD Bays which are each fileld with 14TB disks, and two M.2 NVMe slots on the inside of the left HDD bay which I inserted two 512GB sticks I scraped from old laptops. Just considering data resiliency itself is a fun little adventure into learning about [RAID](*TODO*) and filesystems supporting multi-disk arrays. Questions like "how fast do I need to write data", "how much data am I willing to lose", "what is the minimum storage I want available as actual storage" start to become relevant. I'm going to cut out a bunch of research and give highlights on my decisions for my partition setup: +Before I can install NixOS, I need to identify a partition scheme given this is a server-like Linux install focused on +bulk storage. With this NAS, I have a 32GB internal eMMC (which currently holds bootloaders and the UGREEN OS), two HDD +Bays which are each fileld with 14TB disks, and two M.2 NVMe slots on the inside of the left HDD bay which I inserted +two 512GB sticks I scraped from old laptops. Just considering data resiliency itself is a fun little adventure into +learning about [RAID](*TODO*) and filesystems supporting multi-disk arrays. Questions like "how fast do I need to write +data", "how much data am I willing to lose", "what is the minimum storage I want available as actual storage" start to +become relevant. I'm going to cut out a bunch of research and give highlights on my decisions for my partition setup: -- All data/system partitions use [Btrfs](*TODO*). More advanced than [`ext4`](*TODO*) and supports configurable compression and defragmentation: useful for maximizing density of data storage. +- All data/system partitions use [Btrfs](*TODO*). More advanced than [`ext4`](*TODO*) and supports configurable + compression and defragmentation: useful for maximizing density of data storage. - The eMMC uses a fairly simple partition layout (separapte `/boot` and `/`) seen in most simple Linux installs. - I use [`mdadm`](*TODO*) for two separate RAID 0 (Mirror) groups - The two 14TB HDDs are the "archival" RAID pool and intended to be read + slow write with the nature of that data. - - The two 512GB NVMe sticks are the "fast" RAID pool used for system swap, things that need fast writes (i.e. live databases, caches), and more frequently updated data. + - The two 512GB NVMe sticks are the "fast" RAID pool used for system swap, things that need fast writes (i.e. live + databases, caches), and more frequently updated data. > Editor's Note > > Some quick things about the partition setup that I think are important to shout out: > -> - Btrfs does support being the manager of the RAID pool, I couldn't figure out how to easily do it with disko. Plus `mdadm` is easier to hook up alerts to from my experience. -> - I could get away with the "fast" drives not being in a RAID 0 pool if there are backup jobs that regularly copy stuff to the archival RAID pool. But what's the fun in having only one RAID pool. - -After writing the partition configuraiton and executing the steps with disko, I now have empty partitions to install Nix into. Using disko provides the additional benefit that it becomes part of the system derivation and auto generates [systemd mounts](*TODO*) on system rebuilds. But the first install process a bit of "provisioning work" to deal with secrets. Why? [Because you don't want to store unencrypted secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). For this, I use [sops-nix](*TODO*), which allows associating a secrets YAML with a set of SSH Host keys to decrypt against. But those host keys _only exists after the system is installed and run for the first time_. So I have to have a provisional configuration which has the minimum information without encrypted secrets, get the public key, then add the encrypted secrets configuraiton and rebuild the system deivation. A small annoyance but afterward secrets can be declared securely for that machine easily, even across machines if configured as such. I'm not going to dive deeper into secrets management because that is a whole additional design discussion that I want to avoid in this already long post. - -So I have an empty NAS with NixOS installed and configured to be remoted into for management and remote system derivation updates. The next step is to make a [Network File Share](*TODO*). For this I chose [Samba](*TODO*) over NFS because of the need for Windows systems to be able to access it. So I start looking into how to configure Samba. I follow this [convenient NixOS wiki page for Samba](*TODO*), declare the bind mounts for the Samba shares I wanted, then wrote the Samba configuration. Then I rebuild the NAS system config and try to connect from my Windows machine. It fails immediately. And the worst part is that it's a simple "Something went wrong". I start fiddling with the Samba configuration. Maybe I messed up some security settings. I get a different error this time. I revert my changes. Still "something went wrong". I try to scrounge Event Viewer for more information. Nothing. I even try connecting with my iPhone because the Files app supports Samba. That fails with no indication either. Great. It's Christmas day and I am trying to figure out why my Samba configuration is broken. Then... I found it: +> - Btrfs does support being the manager of the RAID pool, I couldn't figure out how to easily do it with disko. Plus +> `mdadm` is easier to hook up alerts to from my experience. +> - I could get away with the "fast" drives not being in a RAID 0 pool if there are backup jobs that regularly copy stuff +> to the archival RAID pool. But what's the fun in having only one RAID pool. + +After writing the partition configuraiton and executing the steps with disko, I now have empty partitions to install Nix +into. Using disko provides the additional benefit that it becomes part of the system derivation and auto generates +[systemd mounts](*TODO*) on system rebuilds. But the first install process a bit of "provisioning work" to deal with +secrets. Why? [Because you don't want to store unencrypted secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). +For this, I use [sops-nix](*TODO*), which allows associating a secrets YAML with a set of SSH Host keys to decrypt against. +But those host keys _only exists after the system is installed and run for the first time_. +So I have to have a provisional configuration which has the minimum information without encrypted secrets, get the public +key, then add the encrypted secrets configuraiton and rebuild the system deivation. A small annoyance, but afterward secrets +can be declared securely for that machine easily, even across machines if configured as such. I'm not going to dive deeper +into secrets management because that is a whole additional design discussion that I want to avoid in this already long post. + +So I have an empty NAS with NixOS installed and configured to be remoted into for management and remote system derivation +updates. The next step is to make a [Network File Share](*TODO*). For this I chose [Samba](*TODO*) over NFS because of +the need for Windows systems to be able to access it*. So I start looking into how to configure Samba. I follow this +[convenient NixOS wiki page for Samba](*TODO*), declare the bind mounts for the Samba shares I wanted, then wrote the +Samba configuration. Then I rebuild the NAS system config and try to connect from my Windows machine. It fails immediately. +And the worst part is that it's a simple "Something went wrong". I start fiddling with the Samba configuration. Maybe I +messed up some security settings. I get a different error this time. I revert my changes. Still "something went wrong". +I try to scrounge Event Viewer for more information. Nothing. I even try connecting with my iPhone because the Files app +supports Samba. That fails with no indication either. Great. It's Christmas day and I am trying to figure out why my Samba +configuration is broken. Then... I found it: _**TODO INSERT BSKY OR TWITTER POST**_ -That is cursed. And also really annoying. But it works. And that's what matters. Samba having to translate how users are defined from Microsoft AD-like user system to some Linux user definition is black magic that I am relieved I do not need to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](<>) at the Btrfs layer. All of this backed up in a RAID 0 mirror that is acessible by any device on my network. +That is cursed. And also really annoying. But it works. And that's what matters. Samba having to translate how users are +defined from Microsoft AD-like user system to some Linux user definition is black magic that I am relieved I do not need +to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. +As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](<>) at the Btrfs layer. All of t +his backed up in a RAID 0 mirror that is acessible by any device on my network. + +> Editor's Note: Technically, [Windows does support NFS v2/v3 as of recently](https://learn.microsoft.com/en-us/windows-server/storage/nfs/nfs-overview) +> but my intial NFS attempts didn't work and I didn't have as many Linux devices to determine if it was a +> Windows issue. But wait, I forgot to mention something that happened before I could even setup Samba. Yet another fun hiccup. ## Once you've been shopping for PC parts, you'll want to beat Sam Altman to death -***TODO also this title probably*** +_**TODO also this title probably**_ -The UGREEN NAS model I purchased only came with 8GB of RAM. While you _could_ use swap space to act as extra RAM, it doesn't always scale that way especially if you have many processes with "hot" pages. I learned this by accident by trying to run NixOS rebuilds of _**TODO What was it again**_ on a 1GB Raspberry Pi 4 before I learned about Nix remote builders. +The UGREEN NAS model I purchased only came with 8GB of RAM. While you _could_ use swap space to act as extra RAM, it +doesn't always scale that way especially if you have many processes with "hot" pages. I learned this by accident by +trying to run NixOS rebuilds of _**TODO What was it again**_ on a 1GB Raspberry Pi 4 before I learned about Nix remote builders. _**TODO INSERT BSKY OR TWITTER POST**_ -Luckily, UGREEN provided the option of upgrading the RAM. But because it was DDR5, I still needed to buy a fresh stick. I checked the specs and bought a 16GB stick of Corsair Vengance DDR5 rated for 4800 MT/s at 1.1V. The same operating conditions as the pre-installed 8GB stick. I install it. It goes well for about 5 hours, then I get a freeze. I eventually realize it's a hard freeze (not even a kernel panic) and did a power cycle. I think nothing of it untill 3 hours later, another freeze. I connect one of my Raspberry Pi HDMI displays and tape it to the side of the display just to monitor the `dmesg` log when it freezes. This time it survived until sometime while I was sleeping and now I don't have another data point to use for troubleshooting. I power cycle yet again, it freezes in 30 minutes! Each time I just let the system run idle with the exception of `htop` on the main TTY just to get a visual indicator when it freezes. And the freeze is abrupt and silent. No `dmesg`, systemd journal, or magic smoke* is indicative of what could be the issue. - -> Editor's Note: "magic smoke" refers to the mystical gas inside your electronic components that allow it to do things with electricity. Letting that smoke escape will destroy the functionality of the device. - -I start to panic internally. Surely it isn't something wrong with my drives right? I spent $300 per NAS drive and returning/exchanging them would be a massive hassle. I try running without the hard drives. Still get the freeze. I take out the SSD and run with no attached drives. Frozen after 2.5 hours. I start to take apart the NAS from it's chassis just to inspect the motherboard; maybe some magic smoke got trapped in the chassis? After an hour of non-destructive teardown, all the boards look as clean as a PCB should. I spend 2 hours putting it back together in a non-destructive way without a manual and am stumped. Then I think "surely it isn't the RAM?" So I do an experiment and use the pre-installed RAM stick. I turn on the NAS. +Luckily, UGREEN provided the option of upgrading the RAM. But because it was DDR5, I still needed to buy a fresh stick. +I checked the specs and bought a 16GB stick of Corsair Vengance DDR5 rated for 4800 MT/s at 1.1V. The same operating +conditions as the pre-installed 8GB stick. I install it. It goes well for about 5 hours, then I get a freeze. I +eventually realize it's a hard freeze (not even a kernel panic) and did a power cycle. I think nothing of it untill 3 +hours later, another freeze. I connect one of my Raspberry Pi HDMI displays and tape it to the side of the display just +to monitor the `dmesg` log when it freezes. This time it survived until sometime while I was sleeping and now I don't +have another data point to use for troubleshooting. I power cycle yet again, it freezes in 30 minutes! Each time I just +let the system run idle with the exception of `htop` on the main TTY just to get a visual indicator when it freezes. And +the freeze is abrupt and silent. No `dmesg`, systemd journal, or magic smoke* is indicative of what could be the issue. + +> Editor's Note: "magic smoke" refers to the mystical gas inside your electronic components that allow it to do things +> with electricity. Letting that smoke escape will destroy the functionality of the device. + +I start to panic internally. Surely it isn't something wrong with my drives right? I spent $300 per NAS drive and +returning/exchanging them would be a massive hassle. I try running without the hard drives. Still get the freeze. +I take out the SSD and run with no attached drives. Frozen after 2.5 hours. I start to take apart the NAS from it's +chassis just to inspect the motherboard; maybe some magic smoke got trapped in the chassis? After an hour of +non-destructive teardown, all the boards look as clean as a PCB should. I spend 2 hours putting it back together +in a non-destructive way without a manual and am stumped. Then I think "surely it isn't the RAM?" So I do an +experiment and use the pre-installed RAM stick. I turn on the NAS. A full day passes. Didn't freeze yet. I gained the courage to reinsert the SSDs and HDDs and power cycle the machine. @@ -230,16 +350,26 @@ Another full day passes. I check logs and nothing out of the ordinary like befor A second day passes. By this point I am convinced it was the RAM stick. -When I purchased the RAM from Amazon, I spent $200 dollars USD. As of writing, the same stick is now *$400 USD*. I now have a $200 DDR5 paper-weight sitting with my stack of DDR3s and DDR4s, and the worst part is that I think it is actually functional. Since the BIOS of the NAS (and most BIOSes for that matter) don't provide options to change the parameters of the RAM, it could just be a configuration issue of putting a circle into a square hole: it fits, but there is a lot of gaps. Compared to the previous generation, DDR5 has some architectural changes such as moving power management to each DIMM which may be impacting something subtle that I'm just not aware about. I don't have the patience (or budget) to look into this further. So 8GB it is. - -> Editor's Note: the actual length of time to produce the freezes in this section, while random, are purely made up for narrative effect. +When I purchased the RAM from Amazon, I spent $200 dollars USD. As of writing, the same stick is now *$400 USD*. I now +have a $200 DDR5 paper-weight sitting with my stack of DDR3s and DDR4s, and the worst part is that I think it is actually +functional. Since the BIOS of the NAS (and most BIOSes for that matter) don't provide options to change the parameters of +the RAM, it could just be a configuration issue of putting a circle into a square hole: it fits, but there is a lot of gaps. +Compared to the previous generation, DDR5 has some architectural changes such as moving power management to each DIMM which +may be impacting something subtle that I'm just not aware about. I don't have the patience (or budget) to look into this +further. So 8GB it is. +> Editor's Note: the actual length of time to produce the freezes in this section, while random, are purely made up for +> narrative effect. So now, I can surely start adding some things to my NAS beyond file storage. Surely that isn't another significant debate? ## You ever what life was like without Docker? -If you work in tech, you probably know about [Docker](***TODO***) and/or container-based deployments. Extend that with [Kubernetes](***TODO***), maybe some [GitOps](***TODO***) solution, and voila! Isolated, self-healing web application deployments that power most websites and apps. But remember: Docker and containers as a concept is fairly new in the history of computers, it was publicly released in _2013_. The original container engine, [Linux Containers](***TODO***) that Docker was built on, didn't even exist until 2008. In the grand scheme of web applications, +If you work in tech, you probably know about [Docker](***TODO***) and/or container-based deployments. Extend that with +[Kubernetes](***TODO***), maybe some [GitOps](***TODO***) solution, and voila! Isolated, self-healing web application +deployments that power most websites and apps. But remember: Docker and containers as a concept is fairly new in the +history of computers, it was publicly released in _2013_. The original container engine, [Linux Containers](***TODO***) +that Docker was built on, didn't even exist until 2008. In the grand scheme of web applications, ## All My Homies Hate DNS From 3b48e7ecd7edce5e2d269134041eae54c1047ec6 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Fri, 4 Sep 2026 00:31:10 -0400 Subject: [PATCH 03/19] Wow I wrote a lot --- src/blog/0012/index.md | 154 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 11 deletions(-) diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index c420aeb..b8a2251 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -57,8 +57,11 @@ evaluation of ***Nix expressions***, which is written using the ***Nix programmi expression will build the package *from source*** and save the resulting derivation into the ***Nix Store*** along with metadata about the hermetic* build environment the derivation contents were compiled under. The contents of the Nix Store is _read-only_ even to root users*. Installation of derivations do not copy files to directories like `/bin` or `/share`, -rather symlinks are instead created to the derivation contents in the Nix Store. To further This paradigm is a bit tricky -to get the hang of initially (I would know I spent a month trying to nail it down), so this example should help. +rather symlinks are instead created to the derivation contents in the Nix Store. From an ecosystem view, _**Nixpkgs**_ +is the package repository _and_ source for building _**NixOS**_ as a system derivation, which is imported into a Nix +expression to provide the ability to _declare available_ (not install) packages and configure installed services. +This paradigm is a bit tricky to get the hang of initially (I would know I spent a month trying to nail it down), +so this example might help. > Editor's Note > @@ -280,7 +283,7 @@ become relevant. I'm going to cut out a bunch of research and give highlights on After writing the partition configuraiton and executing the steps with disko, I now have empty partitions to install Nix into. Using disko provides the additional benefit that it becomes part of the system derivation and auto generates [systemd mounts](*TODO*) on system rebuilds. But the first install process a bit of "provisioning work" to deal with -secrets. Why? [Because you don't want to store unencrypted secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). +secrets. Why? [To prevent writing unencrypted secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). For this, I use [sops-nix](*TODO*), which allows associating a secrets YAML with a set of SSH Host keys to decrypt against. But those host keys _only exists after the system is installed and run for the first time_. So I have to have a provisional configuration which has the minimum information without encrypted secrets, get the public @@ -304,8 +307,8 @@ _**TODO INSERT BSKY OR TWITTER POST**_ That is cursed. And also really annoying. But it works. And that's what matters. Samba having to translate how users are defined from Microsoft AD-like user system to some Linux user definition is black magic that I am relieved I do not need to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. -As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](<>) at the Btrfs layer. All of t -his backed up in a RAID 0 mirror that is acessible by any device on my network. +As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](*TODO*) at the Btrfs layer. All of +this backed up in a RAID 0 mirror that is acessible by any device on my network. > Editor's Note: Technically, [Windows does support NFS v2/v3 as of recently](https://learn.microsoft.com/en-us/windows-server/storage/nfs/nfs-overview) > but my intial NFS attempts didn't work and I didn't have as many Linux devices to determine if it was a @@ -317,9 +320,10 @@ But wait, I forgot to mention something that happened before I could even setup _**TODO also this title probably**_ -The UGREEN NAS model I purchased only came with 8GB of RAM. While you _could_ use swap space to act as extra RAM, it -doesn't always scale that way especially if you have many processes with "hot" pages. I learned this by accident by -trying to run NixOS rebuilds of _**TODO What was it again**_ on a 1GB Raspberry Pi 4 before I learned about Nix remote builders. +The UGREEN NAS model I purchased only came with 8GB of RAM. While swap space _could_ be used to act as extra RAM, it +doesn't always scale that way especially when many processes with "hot" pages are running concurrently. I learned this +by accident by trying to run NixOS rebuilds of _**TODO What was it again**_ on a 1GB Raspberry Pi 4 before I learned +about Nix remote builders. _**TODO INSERT BSKY OR TWITTER POST**_ @@ -365,14 +369,142 @@ So now, I can surely start adding some things to my NAS beyond file storage. Sur ## You ever what life was like without Docker? -If you work in tech, you probably know about [Docker](***TODO***) and/or container-based deployments. Extend that with +When working in tech, one will usually encounter [Docker](***TODO***) and/or container-based deployments. Extend that with [Kubernetes](***TODO***), maybe some [GitOps](***TODO***) solution, and voila! Isolated, self-healing web application deployments that power most websites and apps. But remember: Docker and containers as a concept is fairly new in the -history of computers, it was publicly released in _2013_. The original container engine, [Linux Containers](***TODO***) -that Docker was built on, didn't even exist until 2008. In the grand scheme of web applications, +history of computers, it was publicly released in _2013_. The original container engine, [Linux Containers (LXC)](https://linuxcontainers.org) +that Docker was built on, didn't even exist until 2008. Back when the Dot-Com bubble was bursting, companies were (probably) +"raw-dogging" PHP servers on Linux servers with no isolation*. And that was the direction I was heading, or somewhat. + +> Editor's Note: I have no proof of these claims. But any proof supporting or denying my claims is welcome by email or +> Twitter + +Linux supports many mechanisms to isolate processes, users, files, and the like: [kernel namespaces](***TODO***), [cgroups](***TODO***), +[SELinux](*TODO*), [capabilities](***TODO***), [`chroot`s](*TODO*). In fact, these are the [mechanisms which are used by +LXC](https://linuxcontainers.org/lxc/introduction/) to provide the isolation of containers but use the same kernel. The +thing is though, you don't need a container engine to replicate the same isolation effect when [systemd units](*TODO*) +provide enough of that functionality already. Skimming through the [systemd manpages](***TODO***), there is either a +direct correlation or some similar setting that can replicate the desired behavior. In some aspects, it is more +customizable than Docker given that systemd is the effective backbone of systems it is installed on. For example, this is the +_rendered_ definition of the service to run [Immich's](***TODO***) machine learning module for features like OCR and semantic matching: + +``` +[Unit] +After=network.target postgresql.target +Description=immich machine learning +Requires=postgresql.target + +[Service] +Type=simple +Environment="IMMICH_HOST=localhost" +Environment="IMMICH_PORT=3003" +Environment="LOCALE_ARCHIVE=/nix/store/0da8w48lpq7zgyp7slalhlljjrqn7nsx-glibc-locales-2.42-84/lib/locale/locale-archive" +Environment="MACHINE_LEARNING_CACHE_FOLDER=/var/cache/immich" +Environment="MACHINE_LEARNING_WORKERS=1" +Environment="MACHINE_LEARNING_WORKER_TIMEOUT=120" +Environment="PATH=/nix/store/3qgy8q2j64v2m9jy3a5jmssacbblhd4r-coreutils-9.11/bin:/nix/store/i4kjidyvl8ciwbcwm75g75sriq3knav0-findutils-4.11.0/bin:/nix/store/yhr4ps9rdmb55pgqaq246vlh7xfac1dx-gnugrep-3.12/bin:/nix/store/4nylac9gazdpsid79qyk0jpqm312b2jx-gnused-4.10/bin:/nix/store/sm8d6jpilwdy3bw3yq2lv8rr8jld26pb-systemd-261.2/bin:/nix/store/3qgy8q2j64v2m9jy3a5jmssacbblhd4r-coreutils-9.11/sbin:/nix/store/i4kjidyvl8ciwbcwm75g75sriq3knav0-findutils-4.11.0/sbin:/nix/store/yhr4ps9rdmb55pgqaq246vlh7xfac1dx-gnugrep-3.12/sbin:/nix/store/4nylac9gazdpsid79qyk0jpqm312b2jx-gnused-4.10/sbin:/nix/store/sm8d6jpilwdy3bw3yq2lv8rr8jld26pb-systemd-261.2/sbin" +Environment="TZDIR=/nix/store/p0ff33dca0fbkskqwrl3spvxcn5974h0-tzdata-2026c/share/zoneinfo" +Environment="XDG_CACHE_HOME=/var/cache/immich" + +CacheDirectory=immich +ExecStart=/nix/store/gp19vj6siqb8avzjcr7j2zvchi9ry764-immich-machine-learning-3.1.0/bin/machine-learning + +Slice=system-immich.slice + +User=immich +Group=immich +UMask=0077 + +NoNewPrivileges=true +CapabilityBoundingSet= +PrivateDevices=true +PrivateMounts=true +PrivateTmp=true +PrivateUsers=true +ProtectClock=true +ProtectControlGroups=true +ProtectHome=true +ProtectHostname=true +ProtectKernelLogs=true +ProtectKernelModules=true +ProtectKernelTunables=true +RestrictAddressFamilies=AF_INET +RestrictAddressFamilies=AF_INET6 +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=true +RestrictRealtime=true +RestrictSUIDSGID=true + +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +There is a lot described here in this one file. From the top, the [unit dependencies](***TODO***) of the service is declared, +meaning that the service will only start after the dependencies are successfully _running_; if for some reason Postgres +fails to start, then this service will not start. Afterward, the actual service definition is started with a few environment +variables for application-specific configuration and setting the `PATH` to necessary executables in the Nix Store. Then, +the cool things you can't do easily in Docker begin. First is registering this service under a [slice](***TODO***), +ensuring the service process is run under the correct [cgroup node](***TODO***) for management; Docker does automatically +for each container since this is one of the core mechanisms for isolation. Then the user and group are set for the process, +adding further isolation through the standard Linux access control mechansisms. Dockerfiles allow specifying the user to +run as, but by default it uses `root` and often tends to be ignored. Finally, additional restrictions are placed on the +execution environment of the service. Explicit declarations to enforce limited privileges and sandboxing for filesystem +access, kernel access, sockets, even ownership properties. The [systemd-exec(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html) +manpage is extremely in depth on the tuning capabilities for the execution environment, compared to Docker which +[abstracts most of that away](https://docs.docker.com/engine/security/). + +Do I recommend running things bare-metal for large services? _Absolutely not_. Containers are a really useful software deployment +architecture that will work for most use cases. The main reason I didn't use containers to deploy internal services is because +Nixpkgs already has many modules to generate systemd unit configurations for many services. The one advantage Nix and +its use of systemd does have compared to Docker (and Kubernetes) is that it is much easier to have stable configurations +and system state; because all the service deployments are Nix expressions which point to Nix store derivations, it is +easy to reproduce and rollback as needed through the NixOS management commands. If you use [GitOps](***TODO***) systems +like [Flux](***TODO***), the concept is very similar with the exception that ## All My Homies Hate DNS +_**TODO insert the "it was DNS" meme here**_ + +If you ever had to do anything related to networking, DNS is treated as the boogeyman for most deployment issues, and +for good reason; At work we have alerts in case an automated DNS update fails for triage purposes. In 2021, [Facebook had +one of the most infamous outages due to a DNS mistake that broke the resolution to the Facebook servers](https://en.wikipedia.org/wiki/2021_Facebook_outage). +But what _is_ DNS anyway? + +DNS stands for [Domain Name System](***TODO***) and is used to convert human-readable domains to some IP address. [Cloudflare +has a good explainer on the concept that I will shamelessly adapt](https://www.cloudflare.com/learning/dns/what-is-dns/), +I recommend their resources for a good introduction to most things networking. At it's core, DNS is really just an elaborate index on how to find information. ntEach part of a domain is really just a specific "locator" for an index that will eventually lead to the desired Host IP address. This rough ASCII drawing illustrates the concept: + +``` + RESOLVED -> 8.0.0.86 + (mail.google.com) ◄─────────────────────────────────────────────┐ + ▼ ▼ ▼ │ + ┌──────────────────────────────.com.google.mail │ + │ │ └─────────────────────────┐ │ + ▼ ▼ ▼ │ + 1.2.3.4 ┌─►123.45.67.89 ┌─►8.0.0.85 │ + ┌────────────────────────┐ │ ┌────────────────────────────┐ │ ┌──────────────────────────────┐ │ + │ ROOT DNS INDEX │ │ │ .COM DNS INDEX │ │ │ GOOGLE.COM DNS INDEX │ │ + ├────────────────────────┤ │ ├────────────────────────────┤ │ ├──────────────────────────────┤ │ + │ *.com -> 123.45.67.89 ├─┘ │ *.apple.com -> 29.34.56.20 │ │ │ google.com -> 8.0.0.86 │ │ + ├────────────────────────┤ ├────────────────────────────┤ │ ├──────────────────────────────┤ │ + │ *.net -> 69.67.21.42 │ │ *.bing.com -> 24.68.38.12 │ │ │ mail.google.com -> 8.0.0.86 ├──┘ + ├────────────────────────┤ ├────────────────────────────┤ │ ├──────────────────────────────┤ + │ *.dev -> 86.47.34.84 │ │ *.google.com -> 8.0.0.85 ├──┘ │ plus.google.com -> 127.0.0.1 │ + ├────────────────────────┤ ├────────────────────────────┤ ├──────────────────────────────┤ + │ ... │ │ ... │ │ ... │ + └────────────────────────┘ └────────────────────────────┘ └──────────────────────────────┘ + +NOTE: IP Addresses and DNS Records here are fake to illustrate the point +``` + +This is an incredibly naive and simplified view of DNS, rather the recursive resolver form. There is a lot more like +[Cache resolvers](***TODO***), [nameserver delegation](***TODO***), and many other things that actually power internet routing. +The important thing to recognize is that DNS allows giving convenient, namespaced labels to your servers that clients can use to +get the IP address when they want it. Now the question becomes how can I have DNS records for things just in my home network? This is where self-hosting a DNS server becomes practical. While I do own a public domain, I + ## Thoughts on Nix - Learning Nix From 539600dfd76a16440c297cfb730a066f50f8807c Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sat, 12 Sep 2026 23:03:19 -0400 Subject: [PATCH 04/19] getting closer... --- src/blog/0012/index.md | 99 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index b8a2251..bcf3eb4 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -385,8 +385,9 @@ LXC](https://linuxcontainers.org/lxc/introduction/) to provide the isolation of thing is though, you don't need a container engine to replicate the same isolation effect when [systemd units](*TODO*) provide enough of that functionality already. Skimming through the [systemd manpages](***TODO***), there is either a direct correlation or some similar setting that can replicate the desired behavior. In some aspects, it is more -customizable than Docker given that systemd is the effective backbone of systems it is installed on. For example, this is the -_rendered_ definition of the service to run [Immich's](***TODO***) machine learning module for features like OCR and semantic matching: +customizable than Docker given that systemd is the effective backbone of systems it is installed on. For example, this +is the _rendered_ definition of the service to run [Immich's](***TODO***) machine learning module for features like OCR +and semantic matching: ``` [Unit] @@ -460,9 +461,41 @@ Do I recommend running things bare-metal for large services? _Absolutely not_. C architecture that will work for most use cases. The main reason I didn't use containers to deploy internal services is because Nixpkgs already has many modules to generate systemd unit configurations for many services. The one advantage Nix and its use of systemd does have compared to Docker (and Kubernetes) is that it is much easier to have stable configurations -and system state; because all the service deployments are Nix expressions which point to Nix store derivations, it is +and system state because all the service deployments are Nix expressions which point to Nix store derivations, it is easy to reproduce and rollback as needed through the NixOS management commands. If you use [GitOps](***TODO***) systems -like [Flux](***TODO***), the concept is very similar with the exception that +like [Flux](***TODO***), the concept is similar with the core difference being the underlying mechanism of action to +manage infrastructure. + +### So What Does my NAS Do? + +With a mechanism for hosting isolated applications, I can run many services for different things I want to be able to do. +The only constraint is that I will only be able to access the services while connected to my home network. With that in mind, +these are the following services I chose: + +- [Immich](***TODO***) - Photo Library + - Great Photo Library management app, includes a ML module to allow for extra metadata like object recognition and OCR + like in Apple Photos. + - Has an official mobile app which supports automatic backup to corresponding albums on the server. +- [Navidrome](***TODO***) - Music Library + - Music streaming server built on the [OpenSubsonic](***TODO***) protocol. Supports scrobbling to [Last.fm](https://last.fm) + - [MusicBrainz Picard](***TODO***) and [MusicBee](***TODO***) are used for management of tags and organising the underlying + files + - I use [Nautilie](***TODO***) on my iPhone for storing a subset of my library for offline listening +- [Jellyfin](***TODO***) - Media Library + - I use the official Jellyfin App or Swiftfin on my phone for offline watching +- [Backrest](***TODO***) - Automated off-site [restic](***TODO***) backups + - For the remote storage, I use [Backblase B2](***TODO***) buckets for each "set" I want to back up +- [Linkwarden](***TODO***) - Browser Bookmark Sync + - I use Microsoft Edge on my Windows machine and phone, but Firefox on my laptop. Using the [Floccus](***TODO***) + extension I can automatically synchronize between the two browsers using this as the source of truth +- [Syncthing](***TODO***) - File Synchronization + - I limit the relays to only go through local network discovery\ +- [Garage](***TODO***) - Local network S3 service + - Used for a local Nix binary cache for derivations I build. +- [Actual](***TODO***) - Budget Tracker + +All of these are accessed through an [Nginx](***TODO***) reverse proxy which routes by subdomains. Which is a fun segue to +the next topic... ## All My Homies Hate DNS @@ -475,7 +508,9 @@ But what _is_ DNS anyway? DNS stands for [Domain Name System](***TODO***) and is used to convert human-readable domains to some IP address. [Cloudflare has a good explainer on the concept that I will shamelessly adapt](https://www.cloudflare.com/learning/dns/what-is-dns/), -I recommend their resources for a good introduction to most things networking. At it's core, DNS is really just an elaborate index on how to find information. ntEach part of a domain is really just a specific "locator" for an index that will eventually lead to the desired Host IP address. This rough ASCII drawing illustrates the concept: +I recommend their resources for a good introduction to most things networking. At it's core, DNS is really just an elaborate +index on how to find information. ntEach part of a domain is really just a specific "locator" for an index that will +eventually lead to the desired Host IP address. This rough ASCII drawing illustrates the concept with many simplifications: ``` RESOLVED -> 8.0.0.86 @@ -502,8 +537,58 @@ NOTE: IP Addresses and DNS Records here are fake to illustrate the point This is an incredibly naive and simplified view of DNS, rather the recursive resolver form. There is a lot more like [Cache resolvers](***TODO***), [nameserver delegation](***TODO***), and many other things that actually power internet routing. -The important thing to recognize is that DNS allows giving convenient, namespaced labels to your servers that clients can use to -get the IP address when they want it. Now the question becomes how can I have DNS records for things just in my home network? This is where self-hosting a DNS server becomes practical. While I do own a public domain, I +The important thing to recognize is that DNS allows giving convenient, namespaced labels to your servers that clients can +use to get the IP address when they want it. Now the question becomes how can I have DNS records for things just in my +home network? This is where self-hosting a DNS server becomes practical for a [split horizon DNS setup](***TODO***). +The mechanism behind this is the use of a local intermediate DNS server with a desired subdomain zone that can be resolved +without recursing the full domain. This allows for certain domains to be accessible on a local network without requiring +the local machine or network to have a publicly accessible IP address (and go through a bajillion layers of [NAT](***TODO***)). + +``` + ┌───────────────────────────────┐ + │ Subdomain in "Global" Horizon │ + └───────────────────────────────┘ + + LOCAL NETWORK + ┌──────────────────────────────────────────────────────────────────┐ ┌───────────────────────────┐ + │ │ │ │ + │ ┌──────────────┐ ┌───────────────────┐ │ │ T H E │ + │ │ │ pipes.faceftw.dev? │ │ │ pipes.faceftw.dev? │ │ + │ │ manifold ├────────────────────────►│ durandal ├──┼────────────────────►│ I N T E R W E B S │ + │ │ (DNS client) │ 123.45.67.89 │ (DNS Server) │ │ 123.45.67.89 │ │ + │ │ │◄────────────────────────┤ │◄─┼─────────────────────┤ (Recursive DNS Resolvers) │ + │ └──────────────┘ └───────────────────┘ │ │ │ + │ │ └───────────────────────────┘ + └──────────────────────────────────────────────────────────────────┘ + + +┌──────────────────────────────┐ +│ Subdomain in "Local" Horizon │ +└──────────────────────────────┘ + + LOCAL NETWORK + ┌──────────────────────────────────────────────────────────────────┐ ┌───────────────────────────┐ + │ │ │ │ + │ ┌──────────────┐ ┌───────────────────┐ │ │ T H E │ + │ │ │ immich.faceftw.dev? │ │ │ │ │ + │ │ manifold ├────────────────────────►│ durandal │ │ *no call* │ I N T E R W E B S │ + │ │ (DNS client) │ 192.168.0.172 │ (DNS Server) │ │ (durandal knows) │ │ + │ │ │◄────────────────────────┤ │ │ │ (Recursive DNS Resolvers) │ + │ └──────────────┘ └───────────────────┘ │ │ │ + │ │ └───────────────────────────┘ + └──────────────────────────────────────────────────────────────────┘ +``` + +Most implementations of split horizon DNS have a dedicated internal subdomain _zone_ that is dedicated for the local +horizon, but Cloudflare does not allow for non-enterprise customers to have subdomains as zones (only as dedicated records +which is not the same). The reason I use individual zones for each local subdomain is two-fold: to prevent collision +with the public records (so I can access my website while on my network), and [TLS Certificates](***TODO***). + +TLS is the hidden "hero" of the modern internet since it provides the process to both validate the authenticity +of a server and begin a secure communication session with unique encryption. While the risk of having anyone intercept +my local network traffic between devices is low given I monitor what is connected, having the benefit of encryption +in my communication is always a plus. But the main reason is that many browsers complain when you are + ## Thoughts on Nix From c66128490446ad71fb80deb8818bb0482af4bf50 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Tue, 15 Sep 2026 22:53:30 -0400 Subject: [PATCH 05/19] first draft done!!!! --- src/blog/0012/index.md | 107 ++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 27 deletions(-) diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index bcf3eb4..c007802 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -1,7 +1,7 @@ --- postTitle: "One Must Imagine Sisyphus Happy" excerpt: "20 minute adventure to fix a camera driver, In-and-Out. Surely..." -date: "2026-03-16" +date: "2026-09-20" author: Alex Westerman slug: "0012" keywords: "Nix, NixOS, software development, reproducible builds, devops, CI, CD, home lab, self hosting" @@ -52,9 +52,9 @@ building system images. Then I started digging into how creating installer image bullshit) really started to take place at this point. At a high-level, the building block of Nix/NixOS is the _**derivation**_, which defines how a package is built and -installed, as well as how to run the exectuables in a hermetic*sandbox. Each derivation is constructed through the -evaluation of ***Nix expressions***, which is written using the ***Nix programming language***. Evaluting a Nix -expression will build the package *from source*** and save the resulting derivation into the ***Nix Store*** along with +installed, as well as how to run the exectuables with dynamic linkage*. Each derivation is constructed through the +evaluation of _**Nix expressions**_, which is written using the _**Nix programming language**_. Evaluting a Nix +expression will build the package _from source_** and save the resulting derivation into the _**Nix Store**_ along with metadata about the hermetic* build environment the derivation contents were compiled under. The contents of the Nix Store is _read-only_ even to root users*. Installation of derivations do not copy files to directories like `/bin` or `/share`, rather symlinks are instead created to the derivation contents in the Nix Store. From an ecosystem view, _**Nixpkgs**_ @@ -67,10 +67,10 @@ so this example might help. > > You might have noticed some asterisks on items in the previous section. Some clarifying notes before we move on: > -> 1. With respect to hermetic sandboxes, I will elaborate on this in the final "Thoughts on Nix" section +> 1. This is done through a lot of [`patchelf`](https://github.com/NixOS/patchelf) calls on executables built in derivations +> since the dynamic libraries and linker used are in the Nix store and not in standard [FHS](***TODO***) locations. > 2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the -> compiled blobs into the install path, but that defeats the key point of derivations. More on this in the last section -> as well. +> compiled blobs into the install path, but that defeats the key point of derivations. > 3. Technically, writing to the Nix store is done through the Nix Daemon _**TODO VERIFY**_ Suppose for some reason I don't have [`sed`](https://www.gnu.org/software/sed/) installed on my NixOS machine. In order @@ -586,23 +586,76 @@ with the public records (so I can access my website while on my network), and [T TLS is the hidden "hero" of the modern internet since it provides the process to both validate the authenticity of a server and begin a secure communication session with unique encryption. While the risk of having anyone intercept -my local network traffic between devices is low given I monitor what is connected, having the benefit of encryption -in my communication is always a plus. But the main reason is that many browsers complain when you are - - -## Thoughts on Nix - -- Learning Nix - -- How do packages work (derivation, store, realization) - -- Finagling with System Derivations -- Getting the printer functional-ish, then distracted -- Introducing the NAS - -- Bootstrapping Nix (Making a "hermetic" install) - -- Setting up services -- Networking Time - -- Bootstrapping yet another system - -- It's always DNS -- Nemesis - -- Managing kernel patches - -- Speeding up builds - -- Linux Ricing +my local network traffic between devices is low given I monitor what is connected, having the benefit of encryption +in my communication is always a plus. But the main reason is that many browsers complain when you access a site through +normal unencrypted HTTP (and for obvious reasons). [Cloudflare also has good explainers on TLS and what is needed to secure +connections on the internet](https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/) that I do not want to +botch in this already long post. The only thing that post does not talk about is automating the process of getting and +renewing a strong certificate. Luckily people smarter than me have created the solution: [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment). + +> Editor's Note +> You should watch the movie [_Coyote VS. ACME_](***TODO***)! + +ACME allows people to get certificates for domains they _own_ by performing a security challenge of some kind with an +issuing certificate authority (CA); in my case, I use [Let's Encrypt](***TODO***),. In my case, I use a [DNS-01 Challenge](***TODO***) +which creates a specific `TXT` record via the Cloudflare DNS management APIs that the CA will check to prove ownership. +Afterward, I can use an ACME client to request a new certificate for the domain that is issued by the CA and have it downloaded +automatically to my local machines. The one important caveat is that since I request a wildcard certificate that will apply to +all subdomains of a specific level (i.e. `*.faceftw.dev`), I must use a DNS-01 challenge to prove ownership. I also cannot +request a certificate for any deeper subdomain levels as a wildcard since it requires the subdomain to be a unique, resolvable +DNS zone, which I cannot setup in Cloudflare. Once the certificate is on the machine, Nix provides some nice machinery to +automatically allow `Nginx` to utilize these certificates, automatically reloading after certificate renewal. + +```nix +***TODO*** Get the snippets +``` + +And that's it. There are no more worlds left to conquer. Sisyphus has pushed his rock up a cliff. I surely have everything +I need and I didn't change _anything_ and won't need to fix _anything_ at all in this little endeavor I went through. + +Right...? + +Why is there more text below this? + +## Is Sisyphus Happy? + +Almost a year ago now, I just wanted a stupid camera driver to work on my 3D printer. Arguably, I solved that problem 3- +4 months after starting, but for some reason I kept pushing the metaphorical rock up the hill. I kept wanting something +_more_ out of learning all of this. It was never about the camera in the first place, it was about _doing something new_. +I cut out a good 2 months of learning [Vala](***TODO***), [GTK](***TODO***), and Linux desktop ecosystems for [ricing](***TODO***) +my on the go laptop, which provides minimal value for a machine that I don't use as often. There is also a good few +weeks learning about how to build Linux with patches that I also excluded. I didn't need to wipe the stock OS from my NAS, +hell, I could see the argument I didn't really need buy one in the first place. + +Picking up Nix and NixOS is not a normal decision. As cool as the experience is learning it and the problems it solves, +there are many things that would not let me recommend it for most people: + +- The Nix documentation is scattered, inconsistent, or non-existent; the problem extends to an uneven learning curve which + puts you in the deep end and expects you to put together all the pieces, instead of a "layered" learning approach by + abstracting internals initially (especially for less technical or hobbyist users). +- [Flakes, the way most people use Nix, are still considered experimental due to how they were introduced and implemented](https://discourse.nixos.org/t/why-are-flakes-still-experimental/29317/12), + with only seemingly incremental progress on the upstream project to stabilize it. +- Certain mechanisms like cross-compiling entire system derivations (i.e. for a Raspberry Pi or IoT devices) have to be + done a certain way to prevent compiling _every single derivation with [binfmt emulation](***TODO***)_, which can + significantly increase times to rebuild a system; This _excludes_ the fact that depending on how you define the + cross-compiled derivations to use, it might miss the [Nixpkgs binary cache](***TODO***) and force an unnecessary rebuild. +- The strong chain of provenance in Nix derivations means that + certain dependency updates need to be done on a per-package basis to prevent excessive derivation rebuilds. +- Executables not built in a Nix derivation and require dynamic linkage will not function without patching ELF metadata. + This is related to the chain of provenance mentioned earlier, but it means that all programs either have to be built from + source in Nix derivations or existing binaries need to be patched with Nix store paths to the dyanmic resources. _Always + expect the possibility of needing to figure out why something won't run or how to make something run in a NixOS system_. + +Despite all of these qualms, I'm overally really happy with the journey that stupid camera driver took me down. There are +multiple cases I can point at where I was able to rollback a stupid change such as a bad DNS configuration easily with minimal +to no downtime thanks to NixOS's generation system. It has also never been easier to run a "lean" system and keep track +of what I'm actually installing and using. Keeping software either up-to-date or on the bleeding edge has been much easier +compared to other distributions which have a significant lag time in updating packages. And best of all, _I can remember +what I did to get here_. I know that whenever I make a change that if it works, it will continue to work. No shell scripts +or niche hooks to forget over time, it has to defined so that it is remembered why a certain tweak was made. Overall, the +reasons why I chose Nix in the first place continue to be the reason I chose to continue using it, despite the negatives +and complexity it introduces. + +So is Sisyphus happy? Maybe he is. Because despite knowing that the rock will roll back down, there will be a few seconds +the rock will be at the mountain top because of him and him alone. + From fc218ba1fe1066f2cf75a587036cc0e1d8675443 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Fri, 18 Sep 2026 22:48:48 -0400 Subject: [PATCH 06/19] Initial Editorial Block + dep updates --- biome.json | 25 +- package-lock.json | 1841 +++++++++++++++++++++++++-------------------- package.json | 20 +- src/main.css | 13 + 4 files changed, 1046 insertions(+), 853 deletions(-) diff --git a/biome.json b/biome.json index c6e23c4..0d477e5 100644 --- a/biome.json +++ b/biome.json @@ -1,13 +1,7 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", "files": { - "includes": [ - "**", - "!**/src/components/ui/**", - "!**/src/lib/**", - "!**/dist/**", - "!**/src/assets/**" - ] + "includes": ["**", "!**/src/components/ui/**", "!**/src/lib/**", "!**/dist/**", "!**/src/assets/**"] }, "formatter": { "enabled": true, @@ -27,6 +21,9 @@ }, "linter": { "enabled": true, + "domains": { + "tailwind": "all" + }, "rules": { "recommended": true, "a11y": {}, @@ -119,9 +116,7 @@ "noConsole": { "level": "warn", "options": { - "allow": [ - "log" - ] + "allow": ["log"] } } }, @@ -163,9 +158,7 @@ "indentWidth": 2 } }, - "html":{ - "formatter":{ - - } + "html": { + "formatter": {} } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index e3b760b..643832b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,27 +8,27 @@ "name": "portfolio-website", "version": "0.1.0", "dependencies": { - "@11ty/eleventy": "4.0.0-alpha.6" + "@11ty/eleventy": "4.0.0-alpha.10" }, "devDependencies": { - "@11ty/eleventy-img": "^6", - "@11ty/eleventy-plugin-rss": "^2", - "@biomejs/biome": "^2.3.10", - "@tailwindcss/postcss": "4.1.18", - "@types/node": "25.0.3", + "@11ty/eleventy-img": "7.0.0", + "@11ty/eleventy-plugin-rss": "2.0.4", + "@biomejs/biome": "^2.5.14", + "@tailwindcss/postcss": "4.3.3", + "@types/node": "26.6.2", "html-minifier-terser": "7.2.0", "luxon": "3.7.2", - "markdown-it": "14.1.0", - "postcss": "8.5.6", - "shiki": "3.20.0", - "tailwindcss": "4.1.18", + "markdown-it": "15.0.2", + "postcss": "8.5.28", + "shiki": "4.4.3", + "tailwindcss": "4.3.3", "typescript": "5.9.3" } }, "node_modules/@11ty/dependency-tree": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.1.tgz", - "integrity": "sha512-6EPI9ZkGU4BX2KNZpWlf4WdV3vrmIWQpn//nAXicTzdPubI3jZlmFdqEv0Yj5M7oavRUGNzw9GbV9cBxhulZWw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.2.tgz", + "integrity": "sha512-RTF6VTZHatYf7fSZBUN3RKwiUeJh5dhWV61gDPrHhQF2/gzruAkYz8yXuvGLx3w3ZBKreGrR+MfYpSVkdbdbLA==", "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.1" @@ -46,52 +46,68 @@ "normalize-path": "^3.0.0" } }, + "node_modules/@11ty/dependency-tree-typescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-typescript/-/dependency-tree-typescript-1.0.0.tgz", + "integrity": "sha512-eQcxtNbMs4LlWV1VeyjPajxO9XMB9ciikqQYnR9oXD6d5o7Fy5jjLyaRCYX0BV5znaDabH1LG4YQZuKGVzHzCg==", + "license": "MIT", + "dependencies": { + "@11ty/dependency-tree-esm": "^2.0.4", + "@sveltejs/acorn-typescript": "^1.0.8", + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@11ty/eleventy": { - "version": "4.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-4.0.0-alpha.6.tgz", - "integrity": "sha512-t83hTloOSTdWnYRusBe0L/oB9imCIP5K1FnMyWDpCI99pe32u1xDFoa9VKh4BHVpd6B2zL6y+Wb1zV1pGv3Dxw==", + "version": "4.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-4.0.0-alpha.10.tgz", + "integrity": "sha512-yAJ3L9l+5GtFZ9c4Zu9dJEUGbxSKGwK7mQDeaZNlNewny9APUHdW8xaPIip5140xvlL7QRFRct1GdPNg93kVGg==", "license": "MIT", "workspaces": [ - "packages/client" + "packages/browser", + "packages/build-awesome" ], "dependencies": { - "@11ty/dependency-tree": "^4.0.1", - "@11ty/dependency-tree-esm": "^2.0.3", - "@11ty/eleventy-dev-server": "^3.0.0-alpha.6", - "@11ty/eleventy-plugin-bundle": "^3.0.7", + "@11ty/dependency-tree": "^4.0.2", + "@11ty/dependency-tree-esm": "^2.0.4", + "@11ty/dependency-tree-typescript": "^1.0.0", + "@11ty/eleventy-dev-server": "^3.0.0-alpha.10", + "@11ty/eleventy-plugin-bundle": "^4.0.2", "@11ty/eleventy-utils": "^2.0.7", - "@11ty/gray-matter": "^2.0.1", + "@11ty/gray-matter": "^2.1.0", "@11ty/lodash-custom": "^4.17.21", "@11ty/node-version-check": "^1.0.1", - "@11ty/nunjucks": "^4.0.0-alpha.1", + "@11ty/nunjucks": "^4.0.0-alpha.3", "@11ty/parse-date-strings": "^2.0.6", - "@11ty/posthtml-urls": "^1.0.2", - "@11ty/recursive-copy": "^5.0.0", + "@11ty/posthtml-urls": "^1.0.3", + "@11ty/recursive-copy": "^5.0.2", "@sindresorhus/slugify": "^3.0.0", "bcp-47-normalize": "^2.3.0", "chokidar": "^5.0.0", - "debug": "^4.4.3", "dependency-graph": "^1.0.0", - "entities": "^7.0.0", + "entities": "^8.0.0", "import-module-string": "^2.0.3", "iso-639-1": "^3.1.5", "kleur": "^4.1.5", - "liquidjs": "^10.24.0", - "markdown-it": "^14.1.0", + "liquidjs": "^10.27.1", + "markdown-it": "^14.2.0", "minimist": "^1.2.8", - "moo": "^0.5.2", + "moo": "0.5.2", "node-retrieve-globals": "^6.0.1", - "picomatch": "^4.0.3", + "obug": "^2.1.3", + "picomatch": "^4.0.4", "posthtml": "^0.16.7", "posthtml-match-helper": "^2.0.3", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15" + "semver": "^7.8.5", + "tinyglobby": "^0.2.17" }, "bin": { "eleventy": "cmd.cjs" }, "engines": { - "node": ">=20.19" + "node": ">=22.15" }, "funding": { "type": "opencollective", @@ -99,29 +115,28 @@ } }, "node_modules/@11ty/eleventy-dev-server": { - "version": "3.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-3.0.0-alpha.6.tgz", - "integrity": "sha512-ZfQL/HG0UB2Vyr9k4VCQilCzbMNjc+/887w7eKN5xCf0/a3bWRxXAxreX71UFhIXafH6DFuRYKUQ4mbYhYjiSg==", + "version": "3.0.0-alpha.11", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-3.0.0-alpha.11.tgz", + "integrity": "sha512-7x9gd7h5HXYECzQ36y/luFSxXIpyjMbQmukle3BXN3LBGA93IZprtgw3cjck6t4t5MlihwVKdcFPiWjg9sVX4g==", "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.7", "@11ty/node-version-check": "^1.0.1", "chokidar": "^5.0.0", - "debug": "^4.4.3", "finalhandler": "^2.1.1", "mime": "^4.1.0", "minimist": "^1.2.8", - "morphdom": "^2.7.7", - "send": "^1.2.0", - "ssri": "^13.0.0", + "morphdom": "^2.7.8", + "obug": "^2.1.4", + "range-parser": "^1.3.0", "urlpattern-polyfill": "^10.1.0", - "ws": "^8.18.3" + "ws": "^8.21.3" }, "bin": { "eleventy-dev-server": "cmd.cjs" }, "engines": { - "node": ">=20.19" + "node": ">=22.15" }, "funding": { "type": "opencollective", @@ -129,16 +144,16 @@ } }, "node_modules/@11ty/eleventy-fetch": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-fetch/-/eleventy-fetch-5.1.1.tgz", - "integrity": "sha512-/xFJLCrqKlcnRKIfO9Qjd1QOs4IpvypljXET955+EgdRPFA+h8Or6bDnZBbcwr6KS7yeUuzp5k1DhXgbentSTA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-fetch/-/eleventy-fetch-5.1.3.tgz", + "integrity": "sha512-4HS6QB/mVWTVlE6kjCKPkjkC1Z0YOqizJaHR05zK+E7a2b4EUC3T+wLktIy7wEl76ZoarkY45EKLmTw1XuR8fQ==", "dev": true, "license": "MIT", "dependencies": { "@11ty/eleventy-utils": "^2.0.7", - "@rgrove/parse-xml": "^4.2.0", + "@rgrove/parse-xml": "^4.2.1", "debug": "^4.4.3", - "flatted": "^3.3.3", + "flatted": "^3.4.2", "p-queue": "6.6.2" }, "engines": { @@ -149,55 +164,78 @@ "url": "https://opencollective.com/11ty" } }, - "node_modules/@11ty/eleventy-img": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-img/-/eleventy-img-6.0.4.tgz", - "integrity": "sha512-jSy9BmubVs0mN76dcXWfSYDgRU+1+/rq/SxUR3MgIvTUAJRDop5pFW+Z1f56CDcOlEHaiPqHgnfOlqRmJvXl7g==", + "node_modules/@11ty/eleventy-fetch/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@11ty/eleventy-fetch/node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", "dev": true, "license": "MIT", "dependencies": { - "@11ty/eleventy-fetch": "^5.1.0", - "@11ty/eleventy-utils": "^2.0.7", - "brotli-size": "^4.0.0", - "debug": "^4.4.0", - "entities": "^6.0.0", - "image-size": "^1.2.1", - "p-queue": "^6.6.2", - "sharp": "^0.33.5" + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@11ty/eleventy-img/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "node_modules/@11ty/eleventy-fetch/node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, "engines": { - "node": ">=0.12" + "node": ">=8" + } + }, + "node_modules/@11ty/eleventy-img": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-img/-/eleventy-img-7.0.0.tgz", + "integrity": "sha512-4b8BSTd1XZSax3hFg1Iyv3B3Vc2DmuTenbFr7M0+x4H3fNJDYq3Bctc9rDxiD021hzO7v6MPXXCb5HsQT6//Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-fetch": "^5.1.3", + "@11ty/eleventy-utils": "^2.0.7", + "brotli-size": "^4.0.0", + "entities": "^8.0.0", + "obug": "^2.1.4", + "p-queue": "^9.3.3", + "sharp": "^0.35.3" + }, + "engines": { + "node": ">=22" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/11ty" } }, "node_modules/@11ty/eleventy-plugin-bundle": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", - "integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-4.0.3.tgz", + "integrity": "sha512-bduTmaKbw2HMh0+rXxmTYmf5pvtwnPR7RLtJjjfuYdaMzf1GIrZWhm5MCnW+jrYNGkwyaD7iAc0z1aTvSoA9sg==", "license": "MIT", "dependencies": { - "@11ty/eleventy-utils": "^2.0.2", - "debug": "^4.4.0", + "@11ty/eleventy-utils": "^2.0.7", + "obug": "^2.2.1", "posthtml-match-helper": "^2.0.3" }, "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "type": "opencollective", @@ -234,16 +272,78 @@ "url": "https://opencollective.com/11ty" } }, + "node_modules/@11ty/eleventy/node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/@11ty/eleventy/node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/@11ty/eleventy/node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@11ty/eleventy/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/@11ty/gray-matter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@11ty/gray-matter/-/gray-matter-2.0.1.tgz", - "integrity": "sha512-IJtfwNC6pX4lV1PXqf25Tnqlp5wFAg7MyuSzqQ9vWoRJsnRggc+yuOQUT3GiedEhGGu8d19n51LzLlRbMPPYZQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@11ty/gray-matter/-/gray-matter-2.1.0.tgz", + "integrity": "sha512-fNdBOb3MgDz/1UCIoeY4wDuGbp+/3s5y6UrsyfMabECbh/CVycj7Er33JXqSxRwxrRKXcGE1Jipuq/1mODInsQ==", "license": "MIT", "dependencies": { - "js-yaml": "^4.1.1", - "kind-of": "^6.0.3", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" + "js-yaml": "^4.2.0", + "section-matter": "^1.0.0" }, "engines": { "node": ">=11" @@ -279,14 +379,10 @@ } }, "node_modules/@11ty/nunjucks": { - "version": "4.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@11ty/nunjucks/-/nunjucks-4.0.0-alpha.1.tgz", - "integrity": "sha512-9n/Qhxk/U8cTgGTv2OgOmeAd4qMJa/46nOpvdjR3d4K342ghli027qkMvtkO69MfCceBfnyZbZrxOyxfZgOkwg==", - "license": "BSD-2-Clause", - "dependencies": { - "a-sync-waterfall": "^1.0.1", - "asap": "^2.0.6" - } + "version": "4.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@11ty/nunjucks/-/nunjucks-4.0.0-alpha.3.tgz", + "integrity": "sha512-oWDW+MtBq8dKrx/njAjCXxrThS5q0Fvaj632a9RwUqKhddeOAgqKN5odvev/PSpkSTGhrLRkaLkjAVrvqrdKsQ==", + "license": "BSD-2-Clause" }, "node_modules/@11ty/parse-date-strings": { "version": "2.0.6", @@ -302,9 +398,9 @@ } }, "node_modules/@11ty/posthtml-urls": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.2.tgz", - "integrity": "sha512-0vaV3Wt0surZ+oS1VdKKe0axeeupuM+l7W/Z866WFQwF+dGg2Tc/nmhk/5l74/Y55P8KyImnLN9CdygNw2huHg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.3.tgz", + "integrity": "sha512-1YvhnkaNlFnnJic1rBMWmTC2adbuy+JQiBfl1Hecr1Wjjik1pQZmGyk/eC9zKX/FQv52s2Nht1Gi/UwhYqrBeg==", "license": "MIT", "dependencies": { "evaluate-value": "^2.0.0", @@ -317,23 +413,23 @@ } }, "node_modules/@11ty/recursive-copy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-5.0.0.tgz", - "integrity": "sha512-ifW4b1SHo8O23HnTtkp00RaTCfNmp36qCXp8J7nPHPyd/E7o2OsmSoAp0+oIX92VaFIhC5ip/ioFgns54HyAbg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-5.0.2.tgz", + "integrity": "sha512-3fzosXvQEyGl7vVXkJ+8ixlhu+BP0Gw9bGKtPuP8JPYJEfEgSc8w86pxjgMq/WNfAs0vPQWOO4lFdAZ7DEbN9g==", "license": "ISC", "dependencies": { "errno": "^1.0.0", "junk": "^3.1.0", - "minimatch": "^10.1.1" + "minimatch": "^10.2.4" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", "dev": true, "license": "MIT", "engines": { @@ -344,9 +440,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.10.tgz", - "integrity": "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.14.tgz", + "integrity": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -360,20 +456,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.10", - "@biomejs/cli-darwin-x64": "2.3.10", - "@biomejs/cli-linux-arm64": "2.3.10", - "@biomejs/cli-linux-arm64-musl": "2.3.10", - "@biomejs/cli-linux-x64": "2.3.10", - "@biomejs/cli-linux-x64-musl": "2.3.10", - "@biomejs/cli-win32-arm64": "2.3.10", - "@biomejs/cli-win32-x64": "2.3.10" + "@biomejs/cli-darwin-arm64": "2.5.14", + "@biomejs/cli-darwin-x64": "2.5.14", + "@biomejs/cli-linux-arm64": "2.5.14", + "@biomejs/cli-linux-arm64-musl": "2.5.14", + "@biomejs/cli-linux-x64": "2.5.14", + "@biomejs/cli-linux-x64-musl": "2.5.14", + "@biomejs/cli-win32-arm64": "2.5.14", + "@biomejs/cli-win32-x64": "2.5.14" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.10.tgz", - "integrity": "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.14.tgz", + "integrity": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", "cpu": [ "arm64" ], @@ -388,9 +484,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.10.tgz", - "integrity": "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.14.tgz", + "integrity": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", "cpu": [ "x64" ], @@ -405,13 +501,16 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.10.tgz", - "integrity": "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.14.tgz", + "integrity": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -422,13 +521,16 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.10.tgz", - "integrity": "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.14.tgz", + "integrity": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -439,13 +541,16 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.10.tgz", - "integrity": "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.14.tgz", + "integrity": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -456,13 +561,16 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.10.tgz", - "integrity": "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.14.tgz", + "integrity": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -473,9 +581,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.10.tgz", - "integrity": "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.14.tgz", + "integrity": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", "cpu": [ "arm64" ], @@ -490,9 +598,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.10.tgz", - "integrity": "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==", + "version": "2.5.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.14.tgz", + "integrity": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", "cpu": [ "x64" ], @@ -507,9 +615,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", "optional": true, @@ -517,10 +625,20 @@ "tslib": "^2.4.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -531,19 +649,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -554,19 +672,39 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -581,9 +719,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -598,13 +736,16 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -615,13 +756,56 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -632,13 +816,16 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -649,13 +836,16 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -666,13 +856,16 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -683,13 +876,16 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -700,167 +896,274 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -871,16 +1174,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -891,7 +1194,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -941,9 +1244,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -959,9 +1262,9 @@ } }, "node_modules/@rgrove/parse-xml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.0.tgz", - "integrity": "sha512-UuBOt7BOsKVOkFXRe4Ypd/lADuNIfqJXv8GvHqtXaTYXPPKkj2nS2zPllVsrtRjcomDhIJVBnZwfmlI222WH8g==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.3.tgz", + "integrity": "sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==", "dev": true, "license": "ISC", "engines": { @@ -969,70 +1272,104 @@ } }, "node_modules/@shikijs/core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.20.0.tgz", - "integrity": "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.20.0", + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", + "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.20.0.tgz", - "integrity": "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.20.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.20.0.tgz", - "integrity": "sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.20.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/langs": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz", - "integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.20.0" + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz", - "integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.20.0" + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz", - "integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", "dev": true, "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/vscode-textmate": { @@ -1043,9 +1380,9 @@ "license": "MIT" }, "node_modules/@sindresorhus/slugify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-3.0.0.tgz", - "integrity": "sha512-SCrKh1zS96q+CuH5GumHcyQEVPsM4Ve8oE0E6tw7AAhGq50K8ojbTUOQnX/j9Mhcv/AXiIsbCfquovyGOo5fGw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-3.0.1.tgz", + "integrity": "sha512-N1oPvN6sytRKZCskv0L+vSyvnUYi9JplPXZ90lT6EBsijjHrUdoXttb0Lna6pw1TWMe8vlajQnLDtU9KYHqtDg==", "license": "MIT", "dependencies": { "@sindresorhus/transliterate": "^2.0.0", @@ -1059,9 +1396,9 @@ } }, "node_modules/@sindresorhus/transliterate": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-2.3.0.tgz", - "integrity": "sha512-BBgw7tduDCoOKPSjuSxqfqeudXLvu7qCffDZHskdfaBWEFBtr9ecrMLevvGAYYk5fWA1I56wn1DgXWKz7D574g==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-2.3.1.tgz", + "integrity": "sha512-gVaaGtKYMYAMmI8buULVH3A2TXVJ98QiwGwI7ddrWGuGidGC2uRt4FHs22+8iROJ0QTzju9CuMjlVsrvpqsdhA==", "license": "MIT", "engines": { "node": ">=20" @@ -1070,50 +1407,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -1124,13 +1470,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -1141,13 +1487,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -1158,13 +1504,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -1175,13 +1521,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -1192,81 +1538,93 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1282,21 +1640,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -1307,13 +1665,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -1324,27 +1682,27 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" } }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1362,13 +1720,13 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/unist": { @@ -1379,22 +1737,16 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "dev": true, "license": "ISC" }, - "node_modules/a-sync-waterfall": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "license": "MIT" - }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1404,9 +1756,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -1418,14 +1770,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/balanced-match": { "version": "4.0.4", @@ -1437,9 +1783,9 @@ } }, "node_modules/bcp-47": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", - "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -1476,15 +1822,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/brotli-size": { @@ -1579,51 +1925,6 @@ "node": ">= 10.0" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1661,15 +1962,6 @@ } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/dependency-graph": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", @@ -1810,26 +2102,26 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -1874,15 +2166,6 @@ "acorn": "^8.15.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/evaluate-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz", @@ -1893,9 +2176,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, @@ -1950,21 +2233,12 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2096,42 +2370,6 @@ "node": ">= 6" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "dev": true, - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/import-module-string": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/import-module-string/-/import-module-string-2.0.3.tgz", @@ -2143,12 +2381,6 @@ "esm-import-transformer": "^3.0.5" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -2173,13 +2405,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "dev": true, - "license": "MIT" - }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -2206,18 +2431,18 @@ "license": "ISC" }, "node_modules/iso-639-1": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", - "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.6.tgz", + "integrity": "sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==", "license": "MIT", "engines": { "node": ">=6.0" } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -2225,9 +2450,19 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2264,9 +2499,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -2280,23 +2515,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -2315,9 +2550,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -2336,9 +2571,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -2357,9 +2592,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -2378,9 +2613,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -2399,13 +2634,16 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2420,13 +2658,16 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2441,13 +2682,16 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2462,13 +2706,16 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2483,9 +2730,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -2504,9 +2751,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -2525,18 +2772,29 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^3.0.0" } }, "node_modules/liquidjs": { - "version": "10.24.0", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.24.0.tgz", - "integrity": "sha512-TAUNAdgwaAXjjcUFuYVJm9kOVH7zc0mTKxsG9t9Lu4qdWjB2BEblyVIYpjWcmJLMGgiYqnGNJjpNMHx0gp/46A==", + "version": "10.29.0", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.29.0.tgz", + "integrity": "sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==", "license": "MIT", "dependencies": { "commander": "^10.0.0" @@ -2590,33 +2848,49 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.2.tgz", + "integrity": "sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", + "argparse": "^3.0.0", + "entities": "^8.0.0", + "linkify-it": "^6.0.0", + "mdurl": "^2.1.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.2.tgz", + "integrity": "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", @@ -2641,9 +2915,9 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "license": "MIT" }, "node_modules/micromark-util-character": { @@ -2755,38 +3029,13 @@ "node": ">=16" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -2804,15 +3053,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -2820,9 +3060,9 @@ "license": "BSD-3-Clause" }, "node_modules/morphdom": { - "version": "2.7.7", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz", - "integrity": "sha512-04GmsiBcalrSCNmzfo+UjU8tt3PhZJKzcOy+r1FlGA7/zri8wre3I1WkYN9PT3sIeIKfW9bpyElA+VzOg2E24g==", + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz", + "integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==", "license": "MIT" }, "node_modules/ms": { @@ -2832,9 +3072,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -2881,6 +3121,19 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2894,21 +3147,21 @@ } }, "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "dev": true, "license": "MIT" }, "node_modules/oniguruma-to-es": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", - "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "dev": true, "license": "MIT", "dependencies": { - "oniguruma-parser": "^0.12.1", - "regex": "^6.0.1", + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, @@ -2923,33 +3176,33 @@ } }, "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", "dev": true, "license": "MIT", "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", "dev": true, "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/param-case": { @@ -2997,11 +3250,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3010,9 +3262,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -3030,7 +3282,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3043,7 +3295,6 @@ "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", "license": "MIT", - "peer": true, "dependencies": { "posthtml-parser": "^0.11.0", "posthtml-render": "^3.0.0" @@ -3089,9 +3340,9 @@ } }, "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "dev": true, "license": "MIT", "funding": { @@ -3114,29 +3365,23 @@ "node": ">=6" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -3197,9 +3442,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3208,103 +3453,74 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "dev": true, - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shiki": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.20.0.tgz", - "integrity": "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "3.20.0", - "@shikijs/engine-javascript": "3.20.0", - "@shikijs/engine-oniguruma": "3.20.0", - "@shikijs/langs": "3.20.0", - "@shikijs/themes": "3.20.0", - "@shikijs/types": "3.20.0", + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/source-map": { @@ -3349,18 +3565,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3385,26 +3589,17 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -3416,9 +3611,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3442,13 +3637,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3457,15 +3652,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -3499,15 +3685,16 @@ } }, "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "dev": true, "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, @@ -3554,9 +3741,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dev": true, "license": "MIT", "dependencies": { @@ -3621,9 +3808,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index bcd87e3..b527667 100644 --- a/package.json +++ b/package.json @@ -7,20 +7,20 @@ "build": "eleventy" }, "dependencies": { - "@11ty/eleventy": "4.0.0-alpha.6" + "@11ty/eleventy": "4.0.0-alpha.10" }, "devDependencies": { - "@11ty/eleventy-img": "^6", - "@11ty/eleventy-plugin-rss": "^2", - "@biomejs/biome": "^2.3.10", - "@tailwindcss/postcss": "4.1.18", - "@types/node": "25.0.3", + "@11ty/eleventy-img": "7.0.0", + "@11ty/eleventy-plugin-rss": "2.0.4", + "@biomejs/biome": "^2.5.14", + "@tailwindcss/postcss": "4.3.3", + "@types/node": "26.6.2", "html-minifier-terser": "7.2.0", "luxon": "3.7.2", - "markdown-it": "14.1.0", - "postcss": "8.5.6", - "shiki": "3.20.0", - "tailwindcss": "4.1.18", + "markdown-it": "15.0.2", + "postcss": "8.5.28", + "shiki": "4.4.3", + "tailwindcss": "4.3.3", "typescript": "5.9.3" } } \ No newline at end of file diff --git a/src/main.css b/src/main.css index c7f8385..d35be8f 100644 --- a/src/main.css +++ b/src/main.css @@ -137,6 +137,19 @@ @apply bg-background text-foreground; } + .editorial { + @apply bg-green-100 border-l-8 border-green-600; + } + + .editorial h5 { + @apply text-green-600; + } + + .editorial .editorial-icon svg { + @apply stroke-current fill-current; + } + + /* A Cutout of Tailwind Typography but More for what I want to do */ /* Some parts are commented since I don't think I'm using them now but maybe later */ .prose { From 4ede35a3ca86b316c30971b2453434365ab7af0f Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Fri, 18 Sep 2026 22:57:54 -0400 Subject: [PATCH 07/19] Fix Biome Config --- biome.json | 9 ++++++--- src/main.css | 17 ++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/biome.json b/biome.json index 0d477e5..504b71f 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json", "files": { - "includes": ["**", "!**/src/components/ui/**", "!**/src/lib/**", "!**/dist/**", "!**/src/assets/**"] + "includes": ["**", "!**/src/components/ui", "!**/src/lib", "!**/dist", "!**/src/assets"] }, "formatter": { "enabled": true, @@ -25,7 +25,7 @@ "tailwind": "all" }, "rules": { - "recommended": true, + "preset": "recommended", "a11y": {}, "complexity": { "noBannedTypes": "error", @@ -139,6 +139,9 @@ } }, "css": { + "parser": { + "tailwindDirectives": true + }, "formatter": { "enabled": true, "lineWidth": 120, diff --git a/src/main.css b/src/main.css index d35be8f..7dd9535 100644 --- a/src/main.css +++ b/src/main.css @@ -54,7 +54,6 @@ --tw-prose-td-borders: oklch(37.3% 0.034 259.733); } - @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); @@ -111,8 +110,9 @@ @apply w-fit; } -@utility filter-svg-green{ - filter: brightness(0) saturate(100%) invert(56%) sepia(99%) saturate(362%) hue-rotate(90deg) brightness(91%) contrast(94%); +@utility filter-svg-green { + filter: brightness(0) saturate(100%) invert(56%) sepia(99%) saturate(362%) hue-rotate(90deg) brightness(91%) + contrast(94%); } @keyframes fade{ @@ -138,18 +138,17 @@ } .editorial { - @apply bg-green-100 border-l-8 border-green-600; + @apply bg-green-100 border-l-8 border-green-600; } .editorial h5 { - @apply text-green-600; + @apply text-green-600; } .editorial .editorial-icon svg { - @apply stroke-current fill-current; + @apply stroke-current fill-current; } - /* A Cutout of Tailwind Typography but More for what I want to do */ /* Some parts are commented since I don't think I'm using them now but maybe later */ .prose { @@ -249,7 +248,7 @@ color: var(--tw-prose-quotes); border-inline-start-width: 0.25rem; border-inline-start-color: var(--tw-prose-quote-borders); - quotes: "\201C""\201D""\2018""\2019"; + quotes: "\201C" "\201D" "\2018" "\2019"; margin-top: 1.6em; margin-bottom: 1.6em; padding-inline-start: 1em; @@ -539,4 +538,4 @@ margin-bottom: 0; } } -} \ No newline at end of file +} From dc606ab40b21a0828adbb3d80b8ab9d5c2609031 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Fri, 18 Sep 2026 23:25:22 -0400 Subject: [PATCH 08/19] in the weeds of markdown parsing again :( --- eleventy.config.mjs | 11 +++++++++- src/blog/0012/index.md | 49 +++++++++++++++++++++++------------------- src/main.css | 4 ++-- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 73614d2..2d0deb3 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -139,7 +139,7 @@ export default async function (eleventyConfig) { }; //Override the default render rule (irrevocably) - mdLib.renderer.rules.fence = function (tokens, idx, _options, _env, slf) { + mdLib.renderer.rules.fence = function (tokens, idx, options, env, slf) { const token = tokens[idx]; const info = token.info ? mdLib.utils.unescapeAll(token.info).trim() : ''; let fenceName = ''; @@ -163,6 +163,15 @@ export default async function (eleventyConfig) {
${imgCaption}
`; } + if (fenceName === 'editorial') { + const contentRendered = slf.render(token.content, options, env); + return `
+
Editor's Note
+ ${contentRendered} +
+ + `; + } const highlighted = highlight(token.content, fenceName, fenceAttrs); if (highlighted.indexOf(' Editor's Preface -> Some of the solutions to problems in this article could have been solved (faster) by me asking Chat-Gippity or equivalent -> AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage -> unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in -> solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published -> which will explain this decision later. -> -> So don't ask "wHy NoT uSe ClAuDe?" if you see something stupid. Embrace the madness with me and jump into the rabbit hole. -> -> This is less of an "informative" post about something I learned and more of an insane journey with some commentary at -> the end. This is post is my equivalent of _[The Odyssey]_ with an additional mini YouTube video essay. This means I -> will embellish some points for narrative effect, but the core messages I want to convey remain intact. Enjoy this -> story almost 9 months in the making! +```editorial +Some of the solutions to problems in this article could have been solved (faster) by me asking Chat-Gippity or equivalent +AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage +unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in +solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published +which will explain this decision later. + +
+So don't ask "wHy NoT uSe ClAuDe?" if you see something stupid. Embrace the madness with me and jump into the rabbit hole. + + +
+This is less of an "informative" post about something I learned and more of an insane journey with some commentary at +the end. This is post is my equivalent of _[The Odyssey]_ with an additional mini YouTube video essay. This means I +will embellish some points for narrative effect, but the core messages I want to convey remain intact. +
+ +Enjoy this story almost 9 months in the making! +``` Over a year ago, I noticed that the webcam on my 3D printer server suddenly stopped working. I remembered doing a ad-hoc package upgrade and thought maybe one of the libraries is broken. Naively, I checked the `apt` log (I was using Raspbian @@ -271,14 +277,14 @@ become relevant. I'm going to cut out a bunch of research and give highlights on - The two 512GB NVMe sticks are the "fast" RAID pool used for system swap, things that need fast writes (i.e. live databases, caches), and more frequently updated data. -> Editor's Note -> -> Some quick things about the partition setup that I think are important to shout out: -> -> - Btrfs does support being the manager of the RAID pool, I couldn't figure out how to easily do it with disko. Plus -> `mdadm` is easier to hook up alerts to from my experience. -> - I could get away with the "fast" drives not being in a RAID 0 pool if there are backup jobs that regularly copy stuff -> to the archival RAID pool. But what's the fun in having only one RAID pool. +```editorial +Some quick things about the partition setup that I think are important to shout out: + +- Btrfs does support being the manager of the RAID pool, I couldn't figure out how to easily do it with disko. Plus + `mdadm` is easier to hook up alerts to from my experience. +- I could get away with the "fast" drives not being in a RAID 0 pool if there are backup jobs that regularly copy stuff + to the archival RAID pool. But what's the fun in having only one RAID pool. +``` After writing the partition configuraiton and executing the steps with disko, I now have empty partitions to install Nix into. Using disko provides the additional benefit that it becomes part of the system derivation and auto generates @@ -658,4 +664,3 @@ and complexity it introduces. So is Sisyphus happy? Maybe he is. Because despite knowing that the rock will roll back down, there will be a few seconds the rock will be at the mountain top because of him and him alone. - diff --git a/src/main.css b/src/main.css index 7dd9535..e94a06c 100644 --- a/src/main.css +++ b/src/main.css @@ -138,11 +138,11 @@ } .editorial { - @apply bg-green-100 border-l-8 border-green-600; + @apply bg-green-800 border-l-8 border-green-400 px-4 py-2 rounded-xl; } .editorial h5 { - @apply text-green-600; + @apply font-extrabold; } .editorial .editorial-icon svg { From 0bff91ef8a305b08e8729be9c65df05bb7822cdd Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sat, 19 Sep 2026 19:42:38 -0400 Subject: [PATCH 09/19] Initial Link Pass + Fix editorial admonitions --- eleventy.config.mjs | 2 +- src/blog/0012/index.md | 195 ++++++++++++++++++++++------------------- 2 files changed, 104 insertions(+), 93 deletions(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 2d0deb3..1706e27 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -164,7 +164,7 @@ export default async function (eleventyConfig) { `; } if (fenceName === 'editorial') { - const contentRendered = slf.render(token.content, options, env); + const contentRendered = mdLib.render(token.content, env).trim(); return `
Editor's Note
${contentRendered} diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index 6282fb2..d6efdaf 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -12,17 +12,11 @@ Some of the solutions to problems in this article could have been solved (faster AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published -which will explain this decision later. +which will explain this decision later. So don't ask **_"wHy NoT aSk ClAuDe?"_** if you see something stupid. Embrace the madness with me and jump into the rabbit hole. -
-So don't ask "wHy NoT uSe ClAuDe?" if you see something stupid. Embrace the madness with me and jump into the rabbit hole. - - -
This is less of an "informative" post about something I learned and more of an insane journey with some commentary at the end. This is post is my equivalent of _[The Odyssey]_ with an additional mini YouTube video essay. This means I will embellish some points for narrative effect, but the core messages I want to convey remain intact. -
Enjoy this story almost 9 months in the making! ``` @@ -69,15 +63,15 @@ expression to provide the ability to _declare available_ (not install) packages This paradigm is a bit tricky to get the hang of initially (I would know I spent a month trying to nail it down), so this example might help. -> Editor's Note -> -> You might have noticed some asterisks on items in the previous section. Some clarifying notes before we move on: -> -> 1. This is done through a lot of [`patchelf`](https://github.com/NixOS/patchelf) calls on executables built in derivations -> since the dynamic libraries and linker used are in the Nix store and not in standard [FHS](***TODO***) locations. -> 2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the -> compiled blobs into the install path, but that defeats the key point of derivations. -> 3. Technically, writing to the Nix store is done through the Nix Daemon _**TODO VERIFY**_ +```editorial +You might have noticed some asterisks on items in the previous section. Some clarifying notes before we move on: + +1. This is done through a lot of [`patchelf`](https://github.com/NixOS/patchelf) calls on executables built in derivations + since the dynamic libraries and linker used are in the Nix store and not in standard [FHS](***TODO***) locations. +2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the + compiled blobs into the install path, but that defeats the key point of derivations. +3. Technically, writing to the Nix store is done through the Nix Daemon _**TODO VERIFY**_ +``` Suppose for some reason I don't have [`sed`](https://www.gnu.org/software/sed/) installed on my NixOS machine. In order to install it, I need to have a Nix expression defining how to make the derivation for `sed`. In [Nixpkgs](https://github.com/nixos/nixpkgs), @@ -232,10 +226,10 @@ focus more on my descent into homelabbing madness, but Nix solves some interesti distribution and packaging that other existing solutions have had trouble with. The big one is [Dependency/DLL Hell](https://en.wikipedia.org/wiki/Dependency_hell) which was the most likely culprit behind my camera driver issues; Nix derivations being uniquely keyed in evaluation means that I can easily have true control over which depdencies and what version is used when building drivers or systems. Once -I got the derivation for the camera streaming software working after a good chunk of debugging [Video4Linux configuration](*TODO*) -and [Device Tree Overlays](*TODO*) (since default NixOS does not come with the Raspberry Pi OS defaults), it was extremely -cathartic to see it simply _work_ after all that. But by that time, Black Friday just passed and I had a new toy shipped -in the mail. +I got the derivation for the camera streaming software working after a good chunk of debugging [Video4Linux configuration](https://www.linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/index.html) +and [Device Tree Overlays](https://en.wikipedia.org/wiki/Devicetree) (since default NixOS does not come with the Raspberry +Pi OS defaults), it was extremely cathartic to see it simply _work_ after all that. But by that time, Black Friday just +passed and I had a new toy shipped in the mail. ## Network Attaching Your Mom (because she is fat) @@ -250,29 +244,30 @@ and use NixOS instead of my default UGREEN NAS software. Surely it can't be that The first problem is to figure out how to get to a boot menu to even install Linux. After mashing Ctrl + F12 or some combination I get into the BIOS and select the USB drive. I boot into the NixOS live image. I start getting some -disk UUIDs so I can write my partition layout with [disko](*TODO*). But suddenly, after about 3 minutes the NAS reboots. -Just out of nowhere. Maybe the flash drive I had the installer on was bad. So I burned the live image again on a new -flash drive. It reboots after 3 minutes again. I boot in but do nothing. Reboots in 3 minutes. I start questioning if -UGREEN put some bizarre write-protection on their OS. I check the BIOS. It turns out there is a setting which acts as a -[Watchdog Timer](*TODO*) and will restart the machine if it does not get a specific "message" from the running OS. I -turn it off. I boot into the live image. I wait 3 minutes. Then I wait another 2 minutes. Then I wait another 5 minutes -because I'm paraniod. After 3 days of on-off debugging. I have only figured out how to _run the NixOS live image on the -NAS_, I haven't even installed a base system. +disk UUIDs so I can write my partition layout with [disko](https://github.com/nix-community/disko). But suddenly, after +about 3 minutes the NAS reboots. Just out of nowhere. Maybe the flash drive I had the installer on was bad. So I burned +the live image again on a new flash drive. It reboots after 3 minutes again. I boot in but do nothing. Reboots in 3 minutes. +I start questioning if UGREEN put some bizarre write-protection on their OS. I check the BIOS. It turns out there is a +setting which acts as a [Watchdog Timer](https://en.wikipedia.org/wiki/Watchdog_timer) and will restart the machine if +it does not get a specific "message" from the running OS. I turn it off. I boot into the live image. I wait 3 minutes. +Then I wait another 2 minutes. Then I wait another 5 minutes because I'm paraniod. After 3 days of on-off debugging. +I have only figured out how to _run the NixOS live image on the NAS_, I haven't even installed a base system. -This somehow didn't deter me from continuing the madness. +This somehow didn't deter me from continuing. Before I can install NixOS, I need to identify a partition scheme given this is a server-like Linux install focused on bulk storage. With this NAS, I have a 32GB internal eMMC (which currently holds bootloaders and the UGREEN OS), two HDD Bays which are each fileld with 14TB disks, and two M.2 NVMe slots on the inside of the left HDD bay which I inserted two 512GB sticks I scraped from old laptops. Just considering data resiliency itself is a fun little adventure into -learning about [RAID](*TODO*) and filesystems supporting multi-disk arrays. Questions like "how fast do I need to write -data", "how much data am I willing to lose", "what is the minimum storage I want available as actual storage" start to -become relevant. I'm going to cut out a bunch of research and give highlights on my decisions for my partition setup: +learning about [RAID](https://en.wikipedia.org/wiki/RAID) and filesystems supporting multi-disk arrays. Questions like +"how fast do I need to write data", "how much data am I willing to lose", "what is the minimum storage I want available as +actual storage" start to become relevant. I'm going to cut out a bunch of research and give highlights on my decisions for +my partition setup: -- All data/system partitions use [Btrfs](*TODO*). More advanced than [`ext4`](*TODO*) and supports configurable - compression and defragmentation: useful for maximizing density of data storage. +- All data/system partitions use [Btrfs](https://en.wikipedia.org/wiki/Btrfs). More advanced than [`ext4`](https://en.wikipedia.org/wiki/Ext4) + and supports configurable compression and defragmentation: useful for maximizing density of data storage. - The eMMC uses a fairly simple partition layout (separapte `/boot` and `/`) seen in most simple Linux installs. -- I use [`mdadm`](*TODO*) for two separate RAID 0 (Mirror) groups +- I use [`mdadm`](https://en.wikipedia.org/wiki/Mdadm) for two separate RAID 0 (Mirror) groups - The two 14TB HDDs are the "archival" RAID pool and intended to be read + slow write with the nature of that data. - The two 512GB NVMe sticks are the "fast" RAID pool used for system swap, things that need fast writes (i.e. live databases, caches), and more frequently updated data. @@ -298,9 +293,10 @@ can be declared securely for that machine easily, even across machines if config into secrets management because that is a whole additional design discussion that I want to avoid in this already long post. So I have an empty NAS with NixOS installed and configured to be remoted into for management and remote system derivation -updates. The next step is to make a [Network File Share](*TODO*). For this I chose [Samba](*TODO*) over NFS because of -the need for Windows systems to be able to access it*. So I start looking into how to configure Samba. I follow this -[convenient NixOS wiki page for Samba](*TODO*), declare the bind mounts for the Samba shares I wanted, then wrote the +updates. The next step is to make a [Network File Share](https://en.wikipedia.org/wiki/Network_File_System). For this I chose +[SMB via Samba](https://en.wikipedia.org/wiki/Server_Message_Block) over NFS because of the need for Windows systems to be able +to access it without too much extra hassle*. So I start looking into how to configure Samba. I follow this [convenient NixOS wiki +page for Samba](https://nixos.wiki/wiki/Samba), declare the bind mounts for the Samba shares I wanted, then wrote the Samba configuration. Then I rebuild the NAS system config and try to connect from my Windows machine. It fails immediately. And the worst part is that it's a simple "Something went wrong". I start fiddling with the Samba configuration. Maybe I messed up some security settings. I get a different error this time. I revert my changes. Still "something went wrong". @@ -308,17 +304,21 @@ I try to scrounge Event Viewer for more information. Nothing. I even try connect supports Samba. That fails with no indication either. Great. It's Christmas day and I am trying to figure out why my Samba configuration is broken. Then... I found it: -_**TODO INSERT BSKY OR TWITTER POST**_ +
+ +
That is cursed. And also really annoying. But it works. And that's what matters. Samba having to translate how users are defined from Microsoft AD-like user system to some Linux user definition is black magic that I am relieved I do not need to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. -As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](*TODO*) at the Btrfs layer. All of -this backed up in a RAID 0 mirror that is acessible by any device on my network. +As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](https://en.wikipedia.org/wiki/Zstd) +at the Btrfs layer. All of this backed up in a RAID 0 mirror that is acessible by any device on my network. -> Editor's Note: Technically, [Windows does support NFS v2/v3 as of recently](https://learn.microsoft.com/en-us/windows-server/storage/nfs/nfs-overview) -> but my intial NFS attempts didn't work and I didn't have as many Linux devices to determine if it was a -> Windows issue. +```editorial +Technically, [Windows does support NFS v2/v3 as of recently](https://learn.microsoft.com/en-us/windows-server/storage/nfs/nfs-overview) +but my intial NFS attempts didn't work and I didn't have as many Linux devices to determine if it was a +Windows issue. +``` But wait, I forgot to mention something that happened before I could even setup Samba. Yet another fun hiccup. @@ -343,8 +343,10 @@ have another data point to use for troubleshooting. I power cycle yet again, it let the system run idle with the exception of `htop` on the main TTY just to get a visual indicator when it freezes. And the freeze is abrupt and silent. No `dmesg`, systemd journal, or magic smoke* is indicative of what could be the issue. -> Editor's Note: "magic smoke" refers to the mystical gas inside your electronic components that allow it to do things -> with electricity. Letting that smoke escape will destroy the functionality of the device. +```editorial +"Magic smoke" refers to the mystical gas inside your electronic components that allow it to do things with electricity. +Letting that smoke escape will destroy the functionality of the device. +``` I start to panic internally. Surely it isn't something wrong with my drives right? I spent $300 per NAS drive and returning/exchanging them would be a massive hassle. I try running without the hard drives. Still get the freeze. @@ -368,34 +370,40 @@ Compared to the previous generation, DDR5 has some architectural changes such as may be impacting something subtle that I'm just not aware about. I don't have the patience (or budget) to look into this further. So 8GB it is. -> Editor's Note: the actual length of time to produce the freezes in this section, while random, are purely made up for -> narrative effect. +```editorial +The actual length of time to produce the freezes in this section, while random, are purely made up for +narrative effect. +``` So now, I can surely start adding some things to my NAS beyond file storage. Surely that isn't another significant debate? ## You ever what life was like without Docker? -When working in tech, one will usually encounter [Docker](***TODO***) and/or container-based deployments. Extend that with -[Kubernetes](***TODO***), maybe some [GitOps](***TODO***) solution, and voila! Isolated, self-healing web application -deployments that power most websites and apps. But remember: Docker and containers as a concept is fairly new in the -history of computers, it was publicly released in _2013_. The original container engine, [Linux Containers (LXC)](https://linuxcontainers.org) -that Docker was built on, didn't even exist until 2008. Back when the Dot-Com bubble was bursting, companies were (probably) -"raw-dogging" PHP servers on Linux servers with no isolation*. And that was the direction I was heading, or somewhat. - -> Editor's Note: I have no proof of these claims. But any proof supporting or denying my claims is welcome by email or -> Twitter - -Linux supports many mechanisms to isolate processes, users, files, and the like: [kernel namespaces](***TODO***), [cgroups](***TODO***), -[SELinux](*TODO*), [capabilities](***TODO***), [`chroot`s](*TODO*). In fact, these are the [mechanisms which are used by -LXC](https://linuxcontainers.org/lxc/introduction/) to provide the isolation of containers but use the same kernel. The -thing is though, you don't need a container engine to replicate the same isolation effect when [systemd units](*TODO*) -provide enough of that functionality already. Skimming through the [systemd manpages](***TODO***), there is either a -direct correlation or some similar setting that can replicate the desired behavior. In some aspects, it is more +When working in tech, one will usually encounter [Docker](https://www.docker.com/) and/or container-based deployments. +Extend that with [Kubernetes](https://kubernetes.io/), maybe some [GitOps](https://www.redhat.com/en/topics/devops/what-is-gitops) +solution, and voila! Atomic, isolated, self-healing web application deployments that power most websites and apps. +But remember: Docker and containers as a concept is fairly new in the history of computers, it was publicly released +in _2013_. The original container engine, [Linux Containers (LXC)](https://linuxcontainers.org) that Docker was built on, +didn't even exist until 2008. Back when the Dot-Com bubble was bursting, companies were (probably) "raw-dogging" PHP +servers on Linux servers with no isolation*. And that was the direction I was heading, or somewhat. + +```editorial +I have no proof of these claims. But any proof supporting or denying my claims can be sent by email or Twitter dot com +``` + +Linux supports many mechanisms to isolate processes, users, files, and the like: [kernel namespaces](https://en.wikipedia.org/wiki/Linux_namespaces), +[cgroups](https://en.wikipedia.org/wiki/Cgroups), [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), +[capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html), [`chroot`s](https://en.wikipedia.org/wiki/Chroot), +and more. In fact, these are the [mechanisms which are used by LXC](https://linuxcontainers.org/lxc/introduction/) to +provide the isolation of containers but use the same kernel. The thing is though, you don't need a container engine to +replicate the same isolation effect when [systemd units](https://www.freedesktop.org/software/systemd/man/systemd.unit.html) +provide enough of that functionality already. Skimming through the [systemd manpages](https://www.freedesktop.org/software/systemd/man/latest/index.html), +there is either a direct correlation or some similar setting that can replicate the desired behavior. In some aspects, it is more customizable than Docker given that systemd is the effective backbone of systems it is installed on. For example, this is the _rendered_ definition of the service to run [Immich's](***TODO***) machine learning module for features like OCR and semantic matching: -``` +```ini [Unit] After=network.target postgresql.target Description=immich machine learning @@ -468,9 +476,8 @@ architecture that will work for most use cases. The main reason I didn't use con Nixpkgs already has many modules to generate systemd unit configurations for many services. The one advantage Nix and its use of systemd does have compared to Docker (and Kubernetes) is that it is much easier to have stable configurations and system state because all the service deployments are Nix expressions which point to Nix store derivations, it is -easy to reproduce and rollback as needed through the NixOS management commands. If you use [GitOps](***TODO***) systems -like [Flux](***TODO***), the concept is similar with the core difference being the underlying mechanism of action to -manage infrastructure. +easy to reproduce and rollback as needed through the NixOS management commands. If you use GitOps systems like [Flux](https://fluxcd.io/), +the concept is similar with the core difference being the underlying mechanism of action to manage infrastructure. ### So What Does my NAS Do? @@ -486,11 +493,11 @@ these are the following services I chose: - Music streaming server built on the [OpenSubsonic](***TODO***) protocol. Supports scrobbling to [Last.fm](https://last.fm) - [MusicBrainz Picard](***TODO***) and [MusicBee](***TODO***) are used for management of tags and organising the underlying files - - I use [Nautilie](***TODO***) on my iPhone for storing a subset of my library for offline listening + - I use [Nautiline](***TODO***) on my iPhone for storing a subset of my library for offline listening - [Jellyfin](***TODO***) - Media Library - I use the official Jellyfin App or Swiftfin on my phone for offline watching - [Backrest](***TODO***) - Automated off-site [restic](***TODO***) backups - - For the remote storage, I use [Backblase B2](***TODO***) buckets for each "set" I want to back up + - For the remote storage, I use [Backblaze B2](***TODO***) buckets for each "set" I want to back up - [Linkwarden](***TODO***) - Browser Bookmark Sync - I use Microsoft Edge on my Windows machine and phone, but Firefox on my laptop. Using the [Floccus](***TODO***) extension I can automatically synchronize between the two browsers using this as the source of truth @@ -512,10 +519,10 @@ for good reason; At work we have alerts in case an automated DNS update fails fo one of the most infamous outages due to a DNS mistake that broke the resolution to the Facebook servers](https://en.wikipedia.org/wiki/2021_Facebook_outage). But what _is_ DNS anyway? -DNS stands for [Domain Name System](***TODO***) and is used to convert human-readable domains to some IP address. [Cloudflare -has a good explainer on the concept that I will shamelessly adapt](https://www.cloudflare.com/learning/dns/what-is-dns/), +DNS stands for [Domain Name System](https://en.wikipedia.org/wiki/Domain_Name_System) and is used to convert human-readable +domains to some IP address. [Cloudflare has a good explainer on the concept that I will shamelessly adopt](https://www.cloudflare.com/learning/dns/what-is-dns/), I recommend their resources for a good introduction to most things networking. At it's core, DNS is really just an elaborate -index on how to find information. ntEach part of a domain is really just a specific "locator" for an index that will +index on how to find information. Each part of a domain is really just a specific "locator" for an index that will eventually lead to the desired Host IP address. This rough ASCII drawing illustrates the concept with many simplifications: ``` @@ -542,13 +549,14 @@ NOTE: IP Addresses and DNS Records here are fake to illustrate the point ``` This is an incredibly naive and simplified view of DNS, rather the recursive resolver form. There is a lot more like -[Cache resolvers](***TODO***), [nameserver delegation](***TODO***), and many other things that actually power internet routing. -The important thing to recognize is that DNS allows giving convenient, namespaced labels to your servers that clients can -use to get the IP address when they want it. Now the question becomes how can I have DNS records for things just in my -home network? This is where self-hosting a DNS server becomes practical for a [split horizon DNS setup](***TODO***). -The mechanism behind this is the use of a local intermediate DNS server with a desired subdomain zone that can be resolved -without recursing the full domain. This allows for certain domains to be accessible on a local network without requiring -the local machine or network to have a publicly accessible IP address (and go through a bajillion layers of [NAT](***TODO***)). +[autonomous systems](), [nameserver delegation](***TODO***), +and many other things that actually power internet routing. The important thing to recognize is that DNS allows giving +convenient, namespaced labels to your servers that clients can use to get the IP address when they want it. Now the +question becomes how can I have DNS records for things just in my home network? This is where self-hosting a DNS server +becomes practical for a [split horizon DNS setup](https://en.wikipedia.org/wiki/Split-horizon_DNS). The mechanism +behind this is the use of a local intermediate DNS server with a desired subdomain zone that can be resolved without +recursing the full domain. This allows for certain domains to be accessible on a local network without requiring the +local machine or network to have a publicly accessible IP address (and go through a bajillion layers of [NAT](). ``` ┌───────────────────────────────┐ @@ -588,7 +596,7 @@ the local machine or network to have a publicly accessible IP address (and go th Most implementations of split horizon DNS have a dedicated internal subdomain _zone_ that is dedicated for the local horizon, but Cloudflare does not allow for non-enterprise customers to have subdomains as zones (only as dedicated records which is not the same). The reason I use individual zones for each local subdomain is two-fold: to prevent collision -with the public records (so I can access my website while on my network), and [TLS Certificates](***TODO***). +with the public records (so I can access my website while on my network), and [TLS Certificates](https://en.wikipedia.org/wiki/Transport_Layer_Security#Digital_certificates). TLS is the hidden "hero" of the modern internet since it provides the process to both validate the authenticity of a server and begin a secure communication session with unique encryption. While the risk of having anyone intercept @@ -599,18 +607,19 @@ connections on the internet](https://www.cloudflare.com/learning/ssl/transport-l botch in this already long post. The only thing that post does not talk about is automating the process of getting and renewing a strong certificate. Luckily people smarter than me have created the solution: [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment). -> Editor's Note -> You should watch the movie [_Coyote VS. ACME_](***TODO***)! +```editorial +This is not talking about the Looney Tunes ACME Corporation. By the way, you should watch the movie [_Coyote VS. ACME_](https://en.wikipedia.org/wiki/Coyote_vs._Acme)! +``` ACME allows people to get certificates for domains they _own_ by performing a security challenge of some kind with an -issuing certificate authority (CA); in my case, I use [Let's Encrypt](***TODO***),. In my case, I use a [DNS-01 Challenge](***TODO***) +issuing certificate authority (CA); in my case, I use [Let's Encrypt](https://letsencrypt.org/). I use a [DNS-01 Challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) which creates a specific `TXT` record via the Cloudflare DNS management APIs that the CA will check to prove ownership. Afterward, I can use an ACME client to request a new certificate for the domain that is issued by the CA and have it downloaded automatically to my local machines. The one important caveat is that since I request a wildcard certificate that will apply to all subdomains of a specific level (i.e. `*.faceftw.dev`), I must use a DNS-01 challenge to prove ownership. I also cannot request a certificate for any deeper subdomain levels as a wildcard since it requires the subdomain to be a unique, resolvable DNS zone, which I cannot setup in Cloudflare. Once the certificate is on the machine, Nix provides some nice machinery to -automatically allow `Nginx` to utilize these certificates, automatically reloading after certificate renewal. +automatically allow Nginx to utilize these certificates, automatically reloading after certificate renewal. ```nix ***TODO*** Get the snippets @@ -628,10 +637,11 @@ Why is there more text below this? Almost a year ago now, I just wanted a stupid camera driver to work on my 3D printer. Arguably, I solved that problem 3- 4 months after starting, but for some reason I kept pushing the metaphorical rock up the hill. I kept wanting something _more_ out of learning all of this. It was never about the camera in the first place, it was about _doing something new_. -I cut out a good 2 months of learning [Vala](***TODO***), [GTK](***TODO***), and Linux desktop ecosystems for [ricing](***TODO***) -my on the go laptop, which provides minimal value for a machine that I don't use as often. There is also a good few -weeks learning about how to build Linux with patches that I also excluded. I didn't need to wipe the stock OS from my NAS, -hell, I could see the argument I didn't really need buy one in the first place. +I cut out a good 2 months of learning [Vala](https://vala.dev/), [GTK](https://www.gtk.org/), and Linux desktop ecosystems +for [ricing](https://www.reddit.com/r/unixporn/) my on the go laptop, which provides minimal value for a machine that I +don't use as often. There is also a good few weeks learning about how to build Linux with patches that I also excluded. +I didn't need to wipe the stock OS from my NAS, hell, I could see the argument I didn't really need buy one in the first +place. Picking up Nix and NixOS is not a normal decision. As cool as the experience is learning it and the problems it solves, there are many things that would not let me recommend it for most people: @@ -642,9 +652,10 @@ there are many things that would not let me recommend it for most people: - [Flakes, the way most people use Nix, are still considered experimental due to how they were introduced and implemented](https://discourse.nixos.org/t/why-are-flakes-still-experimental/29317/12), with only seemingly incremental progress on the upstream project to stabilize it. - Certain mechanisms like cross-compiling entire system derivations (i.e. for a Raspberry Pi or IoT devices) have to be - done a certain way to prevent compiling _every single derivation with [binfmt emulation](***TODO***)_, which can - significantly increase times to rebuild a system; This _excludes_ the fact that depending on how you define the - cross-compiled derivations to use, it might miss the [Nixpkgs binary cache](***TODO***) and force an unnecessary rebuild. + done a certain way to prevent compiling _every single derivation with [binfmt emulation](https://docs.kernel.org/admin-guide/binfmt-misc.html), + which can significantly increase times to rebuild a system; This _excludes_ the fact that depending on how you define + the cross-compiled derivations to use, it might miss the [Nixpkgs binary cache](https://cache.nixos.org/) and force + an unnecessary rebuild. - The strong chain of provenance in Nix derivations means that certain dependency updates need to be done on a per-package basis to prevent excessive derivation rebuilds. - Executables not built in a Nix derivation and require dynamic linkage will not function without patching ELF metadata. From d49f5d2dcc1baacd6c03a71168ca62b1ab41ff89 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sat, 19 Sep 2026 21:54:02 -0400 Subject: [PATCH 10/19] closer to a final draft --- eleventy.config.mjs | 27 +- src/blog/0012/index.md | 289 +++++++++++---------- src/blog/0012/it_was_dns.webp | Bin 0 -> 17124 bytes src/blog/0012/look_at_all_those_gaems.webp | Bin 0 -> 18580 bytes 4 files changed, 167 insertions(+), 149 deletions(-) create mode 100644 src/blog/0012/it_was_dns.webp create mode 100644 src/blog/0012/look_at_all_those_gaems.webp diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 1706e27..b65ad7e 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -1,6 +1,6 @@ +import { HtmlBasePlugin, IdAttributePlugin, InputPathToUrlTransformPlugin } from '@11ty/eleventy'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve as resolveFile } from 'node:path'; -import { HtmlBasePlugin, IdAttributePlugin, InputPathToUrlTransformPlugin } from '@11ty/eleventy'; // import { feedPlugin } from "@11ty/eleventy-plugin-rss"; import { eleventyImageTransformPlugin } from '@11ty/eleventy-img'; import tailwindcss from '@tailwindcss/postcss'; @@ -8,18 +8,20 @@ import htmlmin from 'html-minifier-terser'; import { DateTime } from 'luxon'; import postcss from 'postcss/lib/postcss'; import { createHighlighterCoreSync, createJavaScriptRegexEngine } from 'shiki'; +// import ansi from 'shiki/langs/ansi.mjs'; import c from 'shiki/langs/c.mjs'; import csharp from 'shiki/langs/csharp.mjs'; import css from 'shiki/langs/css.mjs'; +import html_derivative from 'shiki/langs/html-derivative.mjs'; import html from 'shiki/langs/html.mjs'; import java from 'shiki/langs/java.mjs'; import javascript from 'shiki/langs/javascript.mjs'; +import nix from 'shiki/langs/nix.mjs'; import perl from 'shiki/langs/perl.mjs'; import rust from 'shiki/langs/rust.mjs'; import shell from 'shiki/langs/shell.mjs'; import tsx from 'shiki/langs/tsx.mjs'; import typescript from 'shiki/langs/typescript.mjs'; -import html_derivative from 'shiki/langs/html-derivative.mjs'; import vitesse_dark from 'shiki/themes/vitesse-dark.mjs'; import vitesse_light from 'shiki/themes/vitesse-light.mjs'; @@ -108,7 +110,7 @@ export default async function (eleventyConfig) { //Make a singleton Shiki Highlighter const highlighter = createHighlighterCoreSync({ themes: [vitesse_light, vitesse_dark], - langs: [rust, java, perl, html, tsx, typescript, javascript, c, csharp, shell, css, html_derivative], + langs: [rust, java, perl, html, tsx, typescript, javascript, c, csharp, shell, css, html_derivative, nix], engine: createJavaScriptRegexEngine(), }); const langs = highlighter.getLoadedLanguages(); @@ -138,8 +140,23 @@ export default async function (eleventyConfig) { }); }; - //Override the default render rule (irrevocably) - mdLib.renderer.rules.fence = function (tokens, idx, options, env, slf) { + //Hijack the link normalizer to process the href for common URLs + const normalizeLinkProxy = mdLib.normalizeLink.bind({}); + mdLib.normalizeLink = function (/** @type {string}*/ url) { + const linkMatched = /(wiki|gh):(.*)/.exec(url); + if (linkMatched) { + switch (linkMatched[1]) { + case 'wiki': + return normalizeLinkProxy(`https://en.wikipedia.com/wiki/${linkMatched[2]}`); + case 'gh': + return normalizeLinkProxy(`https://github.com/${linkMatched[2]}`); + } + } + return normalizeLinkProxy(url); + }; + + //Override the default fence render rule (irrevocably) + mdLib.renderer.rules.fence = function (tokens, idx, _options, env, slf) { const token = tokens[idx]; const info = token.info ? mdLib.utils.unescapeAll(token.info).trim() : ''; let fenceName = ''; diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index d6efdaf..d1d823b 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -8,17 +8,15 @@ keywords: "Nix, NixOS, software development, reproducible builds, devops, CI, CD --- ```editorial -Some of the solutions to problems in this article could have been solved (faster) by me asking Chat-Gippity or equivalent -AI services. My [modus operandi](https://en.wikipedia.org/wiki/Modus_operandi) in personal projects avoids AI usage -unless I'm absolutely stumped. As of writing, this has only happened once and in that case it was not helpful in -solving the problem. I'll be writing a post about my thoughts on AI usage after this ~~novella~~ post is published -which will explain this decision later. So don't ask **_"wHy NoT aSk ClAuDe?"_** if you see something stupid. Embrace the madness with me and jump into the rabbit hole. - -This is less of an "informative" post about something I learned and more of an insane journey with some commentary at -the end. This is post is my equivalent of _[The Odyssey]_ with an additional mini YouTube video essay. This means I -will embellish some points for narrative effect, but the core messages I want to convey remain intact. - -Enjoy this story almost 9 months in the making! +Some of the solutions to problems in this article could have been solved (faster) by me asking "Chat-Gippity". +My [modus operandi](wiki:Modus_operandi) in personal projects avoids AI usage unless I'm absolutely stumped. +As of writing, this has only happened once and in that case it was not helpful in solving the problem. I'll +be writing a post about my thoughts on AI usage after this ~~novella~~ post is published which will explain +this decision later. So don't ask **_"wHy NoT aSk ClAuDe?"_** if you see something stupid. Embrace the madness +with me and jump into the rabbit hole. + +I will embellish some points for narrative effect, but the core messages I want to convey remain intact. Enjoy +this story almost 9 months in the making! ``` Over a year ago, I noticed that the webcam on my 3D printer server suddenly stopped working. I remembered doing a ad-hoc @@ -66,15 +64,16 @@ so this example might help. ```editorial You might have noticed some asterisks on items in the previous section. Some clarifying notes before we move on: -1. This is done through a lot of [`patchelf`](https://github.com/NixOS/patchelf) calls on executables built in derivations - since the dynamic libraries and linker used are in the Nix store and not in standard [FHS](***TODO***) locations. +1. This is done through a lot of [`patchelf`](gh:NixOS/patchelf) calls on executables built in derivations + since the dynamic libraries and linker used are in the Nix store and not in standard [FHS](wiki:Filesystem_Hierarchy_Standard) + locations. 2. Technically, the derivation could simply download a compiled binary tarball and the compilation step is copying the compiled blobs into the install path, but that defeats the key point of derivations. -3. Technically, writing to the Nix store is done through the Nix Daemon _**TODO VERIFY**_ +3. Writing to the Nix store is done through the Nix Daemon ``` Suppose for some reason I don't have [`sed`](https://www.gnu.org/software/sed/) installed on my NixOS machine. In order -to install it, I need to have a Nix expression defining how to make the derivation for `sed`. In [Nixpkgs](https://github.com/nixos/nixpkgs), +to install it, I need to have a Nix expression defining how to make the derivation for `sed`. In [Nixpkgs](gh:nixos/nixpkgs), the NixOS package repository and utility library, this is what the Nix expression defining the derivation for `sed` looks like (with some extra stuff to make it potentially a bit more clear): @@ -182,11 +181,10 @@ $ nix eval nixpkgs#gnused «derivation /nix/store/8a41q43mxgkvi3a1pihlp9xiakgqvggx-gnused-4.10.drv» ``` -This store path is then able to be provided in other Nix expressions (both system derivations or other package derivations) -or in temporary environments. For example, if I want a temporary environment which has `sed` available to use in the shell -`PATH`, `nix-shell` can be used like such: +This store path is then able to be provided in other Nix expressions (both system derivations or other package +derivations) or in temporary environments. For example, if I want a temporary environment which has `sed` available to use in the shell `PATH`, `nix-shell` can be used like such: -```shell +``` # Just because sed is available in the local Nix store # it isn't in this shell environment's path $ sed --version @@ -221,15 +219,14 @@ $ env | grep PATH PATH=/bin;/usr/bin;;/nix/store/8a41q43mxgkvi3a1pihlp9xiakgqvggx-gnused-4.10/bin ``` -And that's the fundamentals of Nix (with of abridging). There is a lot I intentionally excluded in this description to -focus more on my descent into homelabbing madness, but Nix solves some interesting problems related to software -distribution and packaging that other existing solutions have had trouble with. The big one is [Dependency/DLL Hell](https://en.wikipedia.org/wiki/Dependency_hell) -which was the most likely culprit behind my camera driver issues; Nix derivations being uniquely keyed in evaluation means -that I can easily have true control over which depdencies and what version is used when building drivers or systems. Once -I got the derivation for the camera streaming software working after a good chunk of debugging [Video4Linux configuration](https://www.linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/index.html) -and [Device Tree Overlays](https://en.wikipedia.org/wiki/Devicetree) (since default NixOS does not come with the Raspberry -Pi OS defaults), it was extremely cathartic to see it simply _work_ after all that. But by that time, Black Friday just -passed and I had a new toy shipped in the mail. +And that's the fundamentals of Nix (with a lot of abridging). There is a lot I intentionally excluded in this description to focus more on my descent into homelabbing madness, but Nix solves some interesting problems related to software distribution and packaging that other existing solutions have had trouble with. The big one is [Dependency/DLL +Hell](wiki:Dependency_hell) which was the most likely culprit behind my camera driver issues; Nix derivations being +uniquely keyed in evaluation means that I can easily have true control over which depdencies and what version is used +when building drivers or systems. Once I got the derivation for the camera streaming software working after a good chunk +of debugging [Video4Linux configuration](https://www.linuxtv.org/downloads/v4l-dvb-apis-new/userspace-api/index.html) +and [Device Tree Overlays](wiki:Devicetree) (since default NixOS does not come with the Raspberry Pi OS defaults), +it was extremely cathartic to see it simply _work_ after all that. But by that time, Black Friday just passed and I had +a new toy shipped in the mail. ## Network Attaching Your Mom (because she is fat) @@ -244,11 +241,11 @@ and use NixOS instead of my default UGREEN NAS software. Surely it can't be that The first problem is to figure out how to get to a boot menu to even install Linux. After mashing Ctrl + F12 or some combination I get into the BIOS and select the USB drive. I boot into the NixOS live image. I start getting some -disk UUIDs so I can write my partition layout with [disko](https://github.com/nix-community/disko). But suddenly, after +disk UUIDs so I can write my partition layout with [disko](gh:nix-community/disko). But suddenly, after about 3 minutes the NAS reboots. Just out of nowhere. Maybe the flash drive I had the installer on was bad. So I burned the live image again on a new flash drive. It reboots after 3 minutes again. I boot in but do nothing. Reboots in 3 minutes. I start questioning if UGREEN put some bizarre write-protection on their OS. I check the BIOS. It turns out there is a -setting which acts as a [Watchdog Timer](https://en.wikipedia.org/wiki/Watchdog_timer) and will restart the machine if +setting which acts as a [Watchdog Timer](wiki:Watchdog_timer) and will restart the machine if it does not get a specific "message" from the running OS. I turn it off. I boot into the live image. I wait 3 minutes. Then I wait another 2 minutes. Then I wait another 5 minutes because I'm paraniod. After 3 days of on-off debugging. I have only figured out how to _run the NixOS live image on the NAS_, I haven't even installed a base system. @@ -259,15 +256,15 @@ Before I can install NixOS, I need to identify a partition scheme given this is bulk storage. With this NAS, I have a 32GB internal eMMC (which currently holds bootloaders and the UGREEN OS), two HDD Bays which are each fileld with 14TB disks, and two M.2 NVMe slots on the inside of the left HDD bay which I inserted two 512GB sticks I scraped from old laptops. Just considering data resiliency itself is a fun little adventure into -learning about [RAID](https://en.wikipedia.org/wiki/RAID) and filesystems supporting multi-disk arrays. Questions like +learning about [RAID](wiki:RAID) and filesystems supporting multi-disk arrays. Questions like "how fast do I need to write data", "how much data am I willing to lose", "what is the minimum storage I want available as actual storage" start to become relevant. I'm going to cut out a bunch of research and give highlights on my decisions for my partition setup: -- All data/system partitions use [Btrfs](https://en.wikipedia.org/wiki/Btrfs). More advanced than [`ext4`](https://en.wikipedia.org/wiki/Ext4) +- All data/system partitions use [Btrfs](wiki:Btrfs). More advanced than [`ext4`](wiki:Ext4) and supports configurable compression and defragmentation: useful for maximizing density of data storage. - The eMMC uses a fairly simple partition layout (separapte `/boot` and `/`) seen in most simple Linux installs. -- I use [`mdadm`](https://en.wikipedia.org/wiki/Mdadm) for two separate RAID 0 (Mirror) groups +- I use [`mdadm`](wiki:Mdadm) for two separate RAID 0 (Mirror) groups - The two 14TB HDDs are the "archival" RAID pool and intended to be read + slow write with the nature of that data. - The two 512GB NVMe sticks are the "fast" RAID pool used for system swap, things that need fast writes (i.e. live databases, caches), and more frequently updated data. @@ -283,26 +280,27 @@ Some quick things about the partition setup that I think are important to shout After writing the partition configuraiton and executing the steps with disko, I now have empty partitions to install Nix into. Using disko provides the additional benefit that it becomes part of the system derivation and auto generates -[systemd mounts](*TODO*) on system rebuilds. But the first install process a bit of "provisioning work" to deal with -secrets. Why? [To prevent writing unencrypted secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). -For this, I use [sops-nix](*TODO*), which allows associating a secrets YAML with a set of SSH Host keys to decrypt against. -But those host keys _only exists after the system is installed and run for the first time_. -So I have to have a provisional configuration which has the minimum information without encrypted secrets, get the public -key, then add the encrypted secrets configuraiton and rebuild the system deivation. A small annoyance, but afterward secrets -can be declared securely for that machine easily, even across machines if configured as such. I'm not going to dive deeper -into secrets management because that is a whole additional design discussion that I want to avoid in this already long post. +[systemd mounts](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html) on system rebuilds. +But the first install process a bit of "provisioning work" to deal with secrets. Why? [To prevent writing unencrypted +secrets in the Nix store](https://nixos.wiki/wiki/Comparison_of_secret_managing_schemes). For this, I use +[sops-nix](gh:mic92/sops-nix), which allows associating a secrets YAML with a set of SSH Host keys to decrypt against. +But those host keys _only exists after the system is installed and run for the first time_. So I have to have a +provisional configuration which has the minimum information without encrypted secrets, get the public key, then add the +encrypted secrets configuraiton and rebuild the system deivation. A small annoyance, but afterward secrets can be declared +securely for that machine easily, even across machines if configured as such. I'm not going to dive deeper into secrets +management because that is a whole additional design discussion that I want to avoid in this already long post. So I have an empty NAS with NixOS installed and configured to be remoted into for management and remote system derivation -updates. The next step is to make a [Network File Share](https://en.wikipedia.org/wiki/Network_File_System). For this I chose -[SMB via Samba](https://en.wikipedia.org/wiki/Server_Message_Block) over NFS because of the need for Windows systems to be able -to access it without too much extra hassle*. So I start looking into how to configure Samba. I follow this [convenient NixOS wiki -page for Samba](https://nixos.wiki/wiki/Samba), declare the bind mounts for the Samba shares I wanted, then wrote the -Samba configuration. Then I rebuild the NAS system config and try to connect from my Windows machine. It fails immediately. -And the worst part is that it's a simple "Something went wrong". I start fiddling with the Samba configuration. Maybe I -messed up some security settings. I get a different error this time. I revert my changes. Still "something went wrong". -I try to scrounge Event Viewer for more information. Nothing. I even try connecting with my iPhone because the Files app -supports Samba. That fails with no indication either. Great. It's Christmas day and I am trying to figure out why my Samba -configuration is broken. Then... I found it: +updates. The next step is to make a [Network File Share](wiki:Network_File_System). For this I chose [SMB via Samba](wiki:Server_Message_Block) +over NFS because of the need for Windows systems to be able to access it without too much extra hassle*. So I start +looking into how to configure Samba. I follow this [convenient NixOS wiki page for Samba](https://nixos.wiki/wiki/Samba), +declare the bind mounts for the Samba shares I wanted, then wrote the Samba configuration. Then I rebuild the NAS system +config and try to connect from my Windows machine. It fails immediately. And the worst part is that it's a simple +"Something went wrong". I start fiddling with the Samba configuration. Maybe I messed up some security settings. +I get a different error this time. I revert my changes. Still "something went wrong". I try to scrounge Event Viewer +for more information. Nothing. I even try connecting with my iPhone because the Files app supports Samba. That fails +with no indication either. Great. It's Christmas day and I am trying to figure out why my Samba configuration is broken. +Then... I found it:
@@ -310,14 +308,16 @@ configuration is broken. Then... I found it: That is cursed. And also really annoying. But it works. And that's what matters. Samba having to translate how users are defined from Microsoft AD-like user system to some Linux user definition is black magic that I am relieved I do not need -to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. -As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](https://en.wikipedia.org/wiki/Zstd) -at the Btrfs layer. All of this backed up in a RAID 0 mirror that is acessible by any device on my network. +to get into. I start migrating over my files, I setup the network volumes, and start downloading my _entire_ Steam library. As of today, it currently sits at 9.5TB of my storage with level 9 [zstd compression](wiki:Zstd) at the Btrfs layer. All of this backed up in a RAID 0 mirror that is acessible by any device on my network. + +```image +src="./look_at_all_those_gaems.webp", 800x434 +In my defense, I have beaten at least 300 of these +``` ```editorial Technically, [Windows does support NFS v2/v3 as of recently](https://learn.microsoft.com/en-us/windows-server/storage/nfs/nfs-overview) -but my intial NFS attempts didn't work and I didn't have as many Linux devices to determine if it was a -Windows issue. +but my intial NFS attempts didn't work and I didn't have as many Linux devices to determine if it was a Windows issue. ``` But wait, I forgot to mention something that happened before I could even setup Samba. Yet another fun hiccup. @@ -328,10 +328,10 @@ _**TODO also this title probably**_ The UGREEN NAS model I purchased only came with 8GB of RAM. While swap space _could_ be used to act as extra RAM, it doesn't always scale that way especially when many processes with "hot" pages are running concurrently. I learned this -by accident by trying to run NixOS rebuilds of _**TODO What was it again**_ on a 1GB Raspberry Pi 4 before I learned -about Nix remote builders. +by accident by trying to run NixOS rebuilds which built [WebKit](https://webkit.org) from source on a 1GB Raspberry Pi +4 before I learned about Nix remote builders. -_**TODO INSERT BSKY OR TWITTER POST**_ + Luckily, UGREEN provided the option of upgrading the RAM. But because it was DDR5, I still needed to buy a fresh stick. I checked the specs and bought a 16GB stick of Corsair Vengance DDR5 rated for 4800 MT/s at 1.1V. The same operating @@ -365,14 +365,13 @@ A second day passes. By this point I am convinced it was the RAM stick. When I purchased the RAM from Amazon, I spent $200 dollars USD. As of writing, the same stick is now *$400 USD*. I now have a $200 DDR5 paper-weight sitting with my stack of DDR3s and DDR4s, and the worst part is that I think it is actually functional. Since the BIOS of the NAS (and most BIOSes for that matter) don't provide options to change the parameters of -the RAM, it could just be a configuration issue of putting a circle into a square hole: it fits, but there is a lot of gaps. -Compared to the previous generation, DDR5 has some architectural changes such as moving power management to each DIMM which -may be impacting something subtle that I'm just not aware about. I don't have the patience (or budget) to look into this -further. So 8GB it is. +the RAM, it could just be a configuration issue of putting a circle into a square hole: it fits, but there is a lot of +gaps. Compared to the previous generation, DDR5 has some architectural changes such as moving power management to each +DIMM which may be impacting something subtle that I'm just not aware about. I don't have the patience (or budget) to +look into this further. So 8GB it is. ```editorial -The actual length of time to produce the freezes in this section, while random, are purely made up for -narrative effect. +The actual length of time to produce the freezes in this section, while random, are purely made up for narrative effect. ``` So now, I can surely start adding some things to my NAS beyond file storage. Surely that isn't another significant debate? @@ -391,17 +390,14 @@ servers on Linux servers with no isolation*. And that was the direction I was he I have no proof of these claims. But any proof supporting or denying my claims can be sent by email or Twitter dot com ``` -Linux supports many mechanisms to isolate processes, users, files, and the like: [kernel namespaces](https://en.wikipedia.org/wiki/Linux_namespaces), -[cgroups](https://en.wikipedia.org/wiki/Cgroups), [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux), -[capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html), [`chroot`s](https://en.wikipedia.org/wiki/Chroot), -and more. In fact, these are the [mechanisms which are used by LXC](https://linuxcontainers.org/lxc/introduction/) to -provide the isolation of containers but use the same kernel. The thing is though, you don't need a container engine to +Linux supports many mechanisms to isolate processes, users, files, and the like: [kernel namespaces](wiki:Linux_namespaces), +[cgroups](wiki:Cgroups), [SELinux](wiki:Security-Enhanced_Linux), [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html), +[`chroot`s](wiki:Chroot), and more. In fact, these are the [mechanisms which are used by LXC](https://linuxcontainers.org/lxc/introduction/) +to provide the isolation of containers but use the same kernel. The thing is though, you don't need a container engine to replicate the same isolation effect when [systemd units](https://www.freedesktop.org/software/systemd/man/systemd.unit.html) provide enough of that functionality already. Skimming through the [systemd manpages](https://www.freedesktop.org/software/systemd/man/latest/index.html), -there is either a direct correlation or some similar setting that can replicate the desired behavior. In some aspects, it is more -customizable than Docker given that systemd is the effective backbone of systems it is installed on. For example, this -is the _rendered_ definition of the service to run [Immich's](***TODO***) machine learning module for features like OCR -and semantic matching: +there is either a direct correlation or some similar setting that can replicate the desired behavior. In some aspects, it is more customizable than Docker given that systemd is the effective backbone of systems it is installed on. For example, this is the _rendered_ definition of the service to run [Immich's machine learning module](https://docs.immich.app/features/ml-hardware-acceleration) +for features like OCR and semantic matching: ```ini [Unit] @@ -457,19 +453,20 @@ RestartSec=3 WantedBy=multi-user.target ``` -There is a lot described here in this one file. From the top, the [unit dependencies](***TODO***) of the service is declared, -meaning that the service will only start after the dependencies are successfully _running_; if for some reason Postgres -fails to start, then this service will not start. Afterward, the actual service definition is started with a few environment -variables for application-specific configuration and setting the `PATH` to necessary executables in the Nix Store. Then, -the cool things you can't do easily in Docker begin. First is registering this service under a [slice](***TODO***), -ensuring the service process is run under the correct [cgroup node](***TODO***) for management; Docker does automatically -for each container since this is one of the core mechanisms for isolation. Then the user and group are set for the process, -adding further isolation through the standard Linux access control mechansisms. Dockerfiles allow specifying the user to -run as, but by default it uses `root` and often tends to be ignored. Finally, additional restrictions are placed on the -execution environment of the service. Explicit declarations to enforce limited privileges and sandboxing for filesystem -access, kernel access, sockets, even ownership properties. The [systemd-exec(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html) -manpage is extremely in depth on the tuning capabilities for the execution environment, compared to Docker which -[abstracts most of that away](https://docs.docker.com/engine/security/). +There is a lot described here in this one file. From the top, the [unit dependencies](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#%5BUnit%5D%20Section%20Options) +of the service is declared, meaning that the service will only start after the dependencies are successfully _running_; +if for some reason Postgres fails to start, then this service will not start. Afterward, the actual service definition +is started with a few environment variables for application-specific configuration and setting the `PATH` to necessary +executables in the Nix Store. Then, the cool things you can't do easily in Docker begin. First is registering this service +under a [slice](https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html#), ensuring the service process +is run under the correct [cgroup node](https://man7.org/linux/man-pages/man7/cgroups.7.html) for compute resource management; +Docker does automatically for each container since this is one of the core mechanisms for isolation. Then the user and +group are set for the process, adding further isolation through the standard Linux access control mechansisms. Dockerfiles +allow specifying the user to run as, but by default it uses `root` and often tends to be ignored. Finally, additional +restrictions are placed on the execution environment of the service. Explicit declarations to enforce limited privileges +and sandboxing for filesystem access, kernel access, sockets, even ownership properties. The [systemd-exec(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html) +manpage is extremely in depth on the tuning capabilities for the execution environment, compared to Docker which [abstracts +most of that away](https://docs.docker.com/engine/security/). Do I recommend running things bare-metal for large services? _Absolutely not_. Containers are a really useful software deployment architecture that will work for most use cases. The main reason I didn't use containers to deploy internal services is because @@ -485,47 +482,51 @@ With a mechanism for hosting isolated applications, I can run many services for The only constraint is that I will only be able to access the services while connected to my home network. With that in mind, these are the following services I chose: -- [Immich](***TODO***) - Photo Library +- [Immich](gh:immich-app/immich) - Photo Library - Great Photo Library management app, includes a ML module to allow for extra metadata like object recognition and OCR like in Apple Photos. - Has an official mobile app which supports automatic backup to corresponding albums on the server. -- [Navidrome](***TODO***) - Music Library - - Music streaming server built on the [OpenSubsonic](***TODO***) protocol. Supports scrobbling to [Last.fm](https://last.fm) - - [MusicBrainz Picard](***TODO***) and [MusicBee](***TODO***) are used for management of tags and organising the underlying +- [Navidrome](gh:navidrome/navidrome) - Music Library + - Music streaming server built on the [OpenSubsonic](opensubsonic/open-subsonic-api) protocol. Supports scrobbling to [Last.fm](https://last.fm) + - [MusicBrainz Picard](gh:metabrainz/picard) and [MusicBee](https://getmusicbee.com/) are used for management of tags and organising the underlying files - - I use [Nautiline](***TODO***) on my iPhone for storing a subset of my library for offline listening -- [Jellyfin](***TODO***) - Media Library + - I use [Nautiline](https://nautiline.app/) on my iPhone for storing a subset of my library for offline listening +- [Jellyfin](gh:jellyfin/jellyfin) - Media Library - I use the official Jellyfin App or Swiftfin on my phone for offline watching -- [Backrest](***TODO***) - Automated off-site [restic](***TODO***) backups - - For the remote storage, I use [Backblaze B2](***TODO***) buckets for each "set" I want to back up -- [Linkwarden](***TODO***) - Browser Bookmark Sync - - I use Microsoft Edge on my Windows machine and phone, but Firefox on my laptop. Using the [Floccus](***TODO***) +- [Backrest](gh:garethgeorge/backrest) - Automated off-site [restic](gh:restic/restic) backups + - For the remote storage, I use [Backblaze B2](https://www.backblaze.com/cloud-storage) buckets for each "set" I + want to back up +- [Linkwarden](gh:linkwarden/linkwarden) - Browser Bookmark Sync + - I use Microsoft Edge on my Windows machine and phone, but Firefox on my laptop. Using the [Floccus](floccusaddon/floccus) extension I can automatically synchronize between the two browsers using this as the source of truth -- [Syncthing](***TODO***) - File Synchronization - - I limit the relays to only go through local network discovery\ -- [Garage](***TODO***) - Local network S3 service +- [Syncthing](gh:syncthing/syncthing) - File Synchronization + - I limit the relays to only go through local network discovery +- [Garage](gh:deuxfleurs-org/garage) - Local network S3 service - Used for a local Nix binary cache for derivations I build. -- [Actual](***TODO***) - Budget Tracker +- [Actual](gh:actualbudget/actual) - Budget Tracker -All of these are accessed through an [Nginx](***TODO***) reverse proxy which routes by subdomains. Which is a fun segue to -the next topic... +All of these are accessed through an [Nginx](gh:nginx/nginx) reverse proxy which routes by subdomains. Which is a fun +segue to the next topic... ## All My Homies Hate DNS -_**TODO insert the "it was DNS" meme here**_ +```image +src="./it_was_dns.webp", 600x358 +Network Engineers be like +``` If you ever had to do anything related to networking, DNS is treated as the boogeyman for most deployment issues, and -for good reason; At work we have alerts in case an automated DNS update fails for triage purposes. In 2021, [Facebook had -one of the most infamous outages due to a DNS mistake that broke the resolution to the Facebook servers](https://en.wikipedia.org/wiki/2021_Facebook_outage). +for good reason; At work we have alerts in case an automated DNS update fails for triage purposes. In 2021, [Facebook +had one of the most infamous outages due to a DNS mistake that broke the resolution to the Facebook servers](wiki:2021_Facebook_outage). But what _is_ DNS anyway? -DNS stands for [Domain Name System](https://en.wikipedia.org/wiki/Domain_Name_System) and is used to convert human-readable +DNS stands for [Domain Name System](wiki:Domain_Name_System) and is used to convert human-readable domains to some IP address. [Cloudflare has a good explainer on the concept that I will shamelessly adopt](https://www.cloudflare.com/learning/dns/what-is-dns/), -I recommend their resources for a good introduction to most things networking. At it's core, DNS is really just an elaborate -index on how to find information. Each part of a domain is really just a specific "locator" for an index that will -eventually lead to the desired Host IP address. This rough ASCII drawing illustrates the concept with many simplifications: +I recommend their resources for a good introduction to most things networking. At it's core, DNS is really just an +elaborate index on how to find information. Each part of a domain is really just a specific "locator" for an index that +will eventually lead to the desired Host IP address. This rough ASCII drawing illustrates the concept with many simplifications: -``` +```plaintext RESOLVED -> 8.0.0.86 (mail.google.com) ◄─────────────────────────────────────────────┐ ▼ ▼ ▼ │ @@ -549,16 +550,16 @@ NOTE: IP Addresses and DNS Records here are fake to illustrate the point ``` This is an incredibly naive and simplified view of DNS, rather the recursive resolver form. There is a lot more like -[autonomous systems](), [nameserver delegation](***TODO***), +[autonomous systems](), [nameserver delegation](wiki:Domain_Name_System#Delegation), and many other things that actually power internet routing. The important thing to recognize is that DNS allows giving convenient, namespaced labels to your servers that clients can use to get the IP address when they want it. Now the question becomes how can I have DNS records for things just in my home network? This is where self-hosting a DNS server -becomes practical for a [split horizon DNS setup](https://en.wikipedia.org/wiki/Split-horizon_DNS). The mechanism -behind this is the use of a local intermediate DNS server with a desired subdomain zone that can be resolved without -recursing the full domain. This allows for certain domains to be accessible on a local network without requiring the -local machine or network to have a publicly accessible IP address (and go through a bajillion layers of [NAT](). +becomes practical for a [split horizon DNS setup](wiki:Split-horizon_DNS). The mechanism behind this is the use of a +local intermediate DNS server with a desired subdomain zone that can be resolved without recursing the full domain. +This allows for certain domains to be accessible on a local network without requiring the local machine or network +to have a publicly accessible IP address (and go through a bajillion layers of [NAT](wiki:Network_address_translation). -``` +```plaintext ┌───────────────────────────────┐ │ Subdomain in "Global" Horizon │ └───────────────────────────────┘ @@ -596,37 +597,39 @@ local machine or network to have a publicly accessible IP address (and go throug Most implementations of split horizon DNS have a dedicated internal subdomain _zone_ that is dedicated for the local horizon, but Cloudflare does not allow for non-enterprise customers to have subdomains as zones (only as dedicated records which is not the same). The reason I use individual zones for each local subdomain is two-fold: to prevent collision -with the public records (so I can access my website while on my network), and [TLS Certificates](https://en.wikipedia.org/wiki/Transport_Layer_Security#Digital_certificates). +with the public records (so I can access my website while on my network), and [TLS Certificates](wiki:Transport_Layer_Security#Digital_certificates). TLS is the hidden "hero" of the modern internet since it provides the process to both validate the authenticity of a server and begin a secure communication session with unique encryption. While the risk of having anyone intercept my local network traffic between devices is low given I monitor what is connected, having the benefit of encryption in my communication is always a plus. But the main reason is that many browsers complain when you access a site through -normal unencrypted HTTP (and for obvious reasons). [Cloudflare also has good explainers on TLS and what is needed to secure -connections on the internet](https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/) that I do not want to -botch in this already long post. The only thing that post does not talk about is automating the process of getting and -renewing a strong certificate. Luckily people smarter than me have created the solution: [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment). +normal unencrypted HTTP (and for obvious reasons). [Cloudflare also has good explainers on TLS and what is needed to +secure connections on the internet](https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/) that I do not +want to botch in this already long post. The only thing that post does not talk about is automating the process of +getting and renewing a strong certificate. Luckily people smarter than me have created the solution: [ACME](wiki:Automatic_Certificate_Management_Environment). ```editorial -This is not talking about the Looney Tunes ACME Corporation. By the way, you should watch the movie [_Coyote VS. ACME_](https://en.wikipedia.org/wiki/Coyote_vs._Acme)! +This is not talking about the Looney Tunes ACME Corporation. By the way, you should watch the movie [_Coyote VS. ACME_](wiki:Coyote_vs._Acme)! ``` ACME allows people to get certificates for domains they _own_ by performing a security challenge of some kind with an issuing certificate authority (CA); in my case, I use [Let's Encrypt](https://letsencrypt.org/). I use a [DNS-01 Challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) which creates a specific `TXT` record via the Cloudflare DNS management APIs that the CA will check to prove ownership. -Afterward, I can use an ACME client to request a new certificate for the domain that is issued by the CA and have it downloaded -automatically to my local machines. The one important caveat is that since I request a wildcard certificate that will apply to -all subdomains of a specific level (i.e. `*.faceftw.dev`), I must use a DNS-01 challenge to prove ownership. I also cannot -request a certificate for any deeper subdomain levels as a wildcard since it requires the subdomain to be a unique, resolvable -DNS zone, which I cannot setup in Cloudflare. Once the certificate is on the machine, Nix provides some nice machinery to -automatically allow Nginx to utilize these certificates, automatically reloading after certificate renewal. +Afterward, I can use an ACME client to request a new certificate for the domain that is issued by the CA and have it +downloaded automatically to my local machines. The one important caveat is that since I request a wildcard certificate +that will apply to all subdomains of a specific level (i.e. `*.faceftw.dev`), I must use a DNS-01 challenge to prove +ownership. I also cannot request a certificate for any deeper subdomain levels as a wildcard since it requires the +subdomain to be a unique, resolvable DNS zone, which I cannot setup in Cloudflare unless I open an enterprise account. +Once the certificate is on the machine, Nix provides some nice machinery to automatically allow Nginx to utilize these +certificates, automatically reloading after certificate renewal. ```nix ***TODO*** Get the snippets ``` -And that's it. There are no more worlds left to conquer. Sisyphus has pushed his rock up a cliff. I surely have everything -I need and I didn't change _anything_ and won't need to fix _anything_ at all in this little endeavor I went through. +And that's it. There are no more worlds left to conquer. Sisyphus has pushed his rock up a cliff. I surely have +everything I need and I didn't change _anything_ and won't need to fix _anything_ at all in this little endeavor I went +through. Right...? @@ -637,11 +640,11 @@ Why is there more text below this? Almost a year ago now, I just wanted a stupid camera driver to work on my 3D printer. Arguably, I solved that problem 3- 4 months after starting, but for some reason I kept pushing the metaphorical rock up the hill. I kept wanting something _more_ out of learning all of this. It was never about the camera in the first place, it was about _doing something new_. -I cut out a good 2 months of learning [Vala](https://vala.dev/), [GTK](https://www.gtk.org/), and Linux desktop ecosystems -for [ricing](https://www.reddit.com/r/unixporn/) my on the go laptop, which provides minimal value for a machine that I -don't use as often. There is also a good few weeks learning about how to build Linux with patches that I also excluded. -I didn't need to wipe the stock OS from my NAS, hell, I could see the argument I didn't really need buy one in the first -place. +I cut out a good 2 months of learning [Vala](https://vala.dev/), [GTK](https://www.gtk.org/), and Linux desktop +ecosystems for [ricing](https://www.reddit.com/r/unixporn/) my on the go laptop, which provides minimal value for a +machine that I don't use as often. There is also a good few weeks learning about how to build Linux with patches that I +also excluded. I didn't need to wipe the stock OS from my NAS, hell, I could see the argument I didn't really need buy +one in the first place. Picking up Nix and NixOS is not a normal decision. As cool as the experience is learning it and the problems it solves, there are many things that would not let me recommend it for most people: @@ -664,14 +667,12 @@ there are many things that would not let me recommend it for most people: expect the possibility of needing to figure out why something won't run or how to make something run in a NixOS system_. Despite all of these qualms, I'm overally really happy with the journey that stupid camera driver took me down. There are -multiple cases I can point at where I was able to rollback a stupid change such as a bad DNS configuration easily with minimal -to no downtime thanks to NixOS's generation system. It has also never been easier to run a "lean" system and keep track -of what I'm actually installing and using. Keeping software either up-to-date or on the bleeding edge has been much easier -compared to other distributions which have a significant lag time in updating packages. And best of all, _I can remember -what I did to get here_. I know that whenever I make a change that if it works, it will continue to work. No shell scripts -or niche hooks to forget over time, it has to defined so that it is remembered why a certain tweak was made. Overall, the -reasons why I chose Nix in the first place continue to be the reason I chose to continue using it, despite the negatives -and complexity it introduces. +multiple cases I can point at where I was able to rollback a stupid change such as a bad DNS configuration easily with +minimal to no downtime thanks to NixOS's generation system. It has also never been easier to run a "lean" system and keep track of what I'm actually installing and using. Keeping software either up-to-date or on the bleeding edge has +been much easier compared to other distributions which have a significant lag time in updating packages. And best of +all, _I can remember what I did to get here_. I know that whenever I make a change that if it works, it will continue to +work. No shell scripts or niche hooks to forget over time, it has to defined so that it is remembered why a certain +tweak was made. Overall, the reasons why I chose Nix in the first place continue to be the reason I chose to continue using it, despite the negatives and complexity it introduces. So is Sisyphus happy? Maybe he is. Because despite knowing that the rock will roll back down, there will be a few seconds the rock will be at the mountain top because of him and him alone. diff --git a/src/blog/0012/it_was_dns.webp b/src/blog/0012/it_was_dns.webp new file mode 100644 index 0000000000000000000000000000000000000000..8ff21efd8a2c38359df43c97cad505788d6725c4 GIT binary patch literal 17124 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 f!iC2-CU1|YEj<9gWY7l*1uf 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 Date: Sat, 19 Sep 2026 21:56:48 -0400 Subject: [PATCH 11/19] Bump CI Actions --- .github/workflows/codeberg-mirror.yaml | 2 +- .github/workflows/deploy.yml | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/codeberg-mirror.yaml b/.github/workflows/codeberg-mirror.yaml index 9e7421b..a865da4 100644 --- a/.github/workflows/codeberg-mirror.yaml +++ b/.github/workflows/codeberg-mirror.yaml @@ -6,7 +6,7 @@ jobs: mirror-repo: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: pixta-dev/repository-mirroring-action@v1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 779b4d4..c37b017 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,18 +12,18 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 22 + node-version: 26 cache: npm cache-dependency-path: ./package-lock.json - name: Load Project Dependencies run: npm install --frozen-lockfile - name: Run Build run: npm run build - - uses: cloudflare/wrangler-action@v3 + - uses: cloudflare/wrangler-action@v4 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -34,24 +34,24 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' && github.event_name == 'push' && github.event_name != 'pull_request' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 22 + node-version: 26 cache: npm cache-dependency-path: package-lock.json - name: Load Project Dependencies run: npm install --frozen-lockfile - name: Run Build run: npm run build - - uses: cloudflare/wrangler-action@v3 + - uses: cloudflare/wrangler-action@v4 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }} - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@v5 with: path: dist @@ -66,5 +66,5 @@ jobs: url: ${{ steps.gh-deploy.outputs.page_url }} needs: [release] steps: - - uses: actions/deploy-pages@v4.0.2 + - uses: actions/deploy-pages@v5.0.1 id: gh-deploy From ba1c005118b452480ad61e9f2659d6998b561807 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sat, 19 Sep 2026 22:17:56 -0400 Subject: [PATCH 12/19] almost forgot that nginx example --- src/blog/0012/index.md | 47 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/blog/0012/index.md b/src/blog/0012/index.md index d1d823b..d224c5e 100644 --- a/src/blog/0012/index.md +++ b/src/blog/0012/index.md @@ -624,7 +624,52 @@ Once the certificate is on the machine, Nix provides some nice machinery to auto certificates, automatically reloading after certificate renewal. ```nix -***TODO*** Get the snippets +### acme.nix - sets up the ACME renewal service and makes +### it available to nginx +{ + config, + pkgs, + ... +}: +{ + security.acme.acceptTerms = true; + security.acme.defaults.email = "alex@faceftw.dev"; + security.acme.certs."faceftw.dev" = { + domain = "*.faceftw.dev"; + dnsProvider = "cloudflare"; + environmentFile = "/run/secrets/cloudflare_acme"; + + postRun = '' + # set permission on dir + ${pkgs.acl}/bin/setfacl -m u:nginx:rx \ + /var/lib/acme/faceftw.dev + + # set permission on key file + ${pkgs.acl}/bin/setfacl -m u:nginx:r \ + /var/lib/acme/faceftw.dev/*.pem + ''; + + reloadServices = [ "nginx" ]; + group = config.services.nginx.group; + }; + +} + +### some service.nix with nginx configured +{ + config, + lib, + ... +}: +{ + services.nginx.virtualHosts."example-service.faceftw.dev" = { + serverName = "example-service.faceftw.dev"; + forceSSL = true; + useACMEHost = "faceftw.dev"; + + # Standard nginx reverse proxy config afterward... + }; +} ``` And that's it. There are no more worlds left to conquer. Sisyphus has pushed his rock up a cliff. I surely have From 50e622fde0e7b46dea22f832f44fbb9e4bd924bb Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sun, 20 Sep 2026 19:21:30 -0400 Subject: [PATCH 13/19] fix compiled css distribution --- eleventy.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index b65ad7e..4e308e8 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -32,7 +32,7 @@ export default async function (eleventyConfig) { ************************/ eleventyConfig.on('eleventy.before', async () => { const tailwindInputPath = resolveFile('./src/main.css'); - const tailwindOutputPath = './src/assets/compiled.css'; + const tailwindOutputPath = './dist/assets/compiled.css'; const cssContent = readFileSync(tailwindInputPath, 'utf8'); const outputDir = dirname(tailwindOutputPath); From 16ec974539b596c44e6ac3df1833a0b4d252ca51 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sun, 20 Sep 2026 19:25:06 -0400 Subject: [PATCH 14/19] wait we have a passthrough --- eleventy.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 4e308e8..b65ad7e 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -32,7 +32,7 @@ export default async function (eleventyConfig) { ************************/ eleventyConfig.on('eleventy.before', async () => { const tailwindInputPath = resolveFile('./src/main.css'); - const tailwindOutputPath = './dist/assets/compiled.css'; + const tailwindOutputPath = './src/assets/compiled.css'; const cssContent = readFileSync(tailwindInputPath, 'utf8'); const outputDir = dirname(tailwindOutputPath); From 70c16302013e5d97908667c5ac120250653df81d Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sun, 20 Sep 2026 19:29:46 -0400 Subject: [PATCH 15/19] maybe this --- eleventy.config.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index b65ad7e..a907e1c 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -32,7 +32,7 @@ export default async function (eleventyConfig) { ************************/ eleventyConfig.on('eleventy.before', async () => { const tailwindInputPath = resolveFile('./src/main.css'); - const tailwindOutputPath = './src/assets/compiled.css'; + const tailwindOutputPath = './dist/assets/styles.css'; const cssContent = readFileSync(tailwindInputPath, 'utf8'); const outputDir = dirname(tailwindOutputPath); @@ -80,7 +80,7 @@ export default async function (eleventyConfig) { './src/assets/fonts': 'assets/fonts', './src/assets/pfp.webp': 'assets/pfp.webp', './src/assets/thumbs': 'assets/thumbs', - './src/assets/compiled.css': 'assets/styles.css', + // './src/assets/compiled.css': 'assets/styles.css', }) .addPassthroughCopy('./content/feed/pretty-atom-feed.xsl'); From 31615eae0c63919117060a4202aa4c66a7f0f644 Mon Sep 17 00:00:00 2001 From: Alex Westerman Date: Sun, 20 Sep 2026 20:14:45 -0400 Subject: [PATCH 16/19] post thumbnail --- src/blog/0012/does_he_know.webp | Bin 0 -> 161000 bytes src/blog/0012/index.md | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 src/blog/0012/does_he_know.webp diff --git a/src/blog/0012/does_he_know.webp b/src/blog/0012/does_he_know.webp new file mode 100644 index 0000000000000000000000000000000000000000..35398f8cc92615d773dc06c9eca4e550639d3738 GIT binary patch literal 161000 zcmV(zK<2+vNk&HCbOHcZMM6+kP&gpebOHeIRS2B{Dv<5fT z>;FUi&&^LA_&f4X$p4@JH~vfhr}|H{&-eIC+}}_i8XkfCulRrRU*|vAe|P`?{^RWz z?ce)P+aK+p|NjktBK^+%^ZFP5HT?_x|Mjo&-}67$fB*U6{j&I-`_1_)@Mrrk_+R&b z<3HU0>G^p7gZ}sY|GfX{zwW>Be}(@`@&o*H`Y-(t+n@R$zyJUK_&t$7$^X~?-T#B& z6Z%j3KlcBdzc2srfAW8i|4IMr{r}o`^-uL*_`lKrs{i}+IsDuF2mHU!?~o7t9-trd zy+A*I{jvNf_8am`_N(WI$3Lb2SN;q3oBO|_e~^6#`S0$3<$vpciTL3A-~Uha-b4K# z`d|G2?7c<)TljPM-{HU0fA0T7|HuC)%OClF)P7(3hxxzpfBHYn{$u=yeqH@{{#W>q z-p|bMvPeAl7xVoFz90Pm{;$q&_kPtsard+D_vK&efBAjme8K+p|8w)7{Fnd#{9kMT zsQ=&p5BHUC01hECh+y;(1F9M3|sDOVVoXL&oDVXLb)(fe?a?JZk;VC2sn0=;2lS@TS=g2zI z2hoAEX8HL3T~uxGZ_2steNW;%a!}q(wtaY@_MZvcEdg~^++FS>^gfIEtdrjF$QXf~ zv{SkOub*_-*4K%{XX3a? z-)MUX3yA}%Tczti?kjM*Ft))FG8r(q0-NWXewq+T{n7^#_A!vl#TGBA803Xeyl&F3 z0SQYh+-suk@{9X02E6q=MUC<$arh$G$8`hGilfs0jE@QkfE}l^6kQx*v|X=094