<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <updated></updated>
  <generator>https://yabu.me</generator>

  <title>Nostr notes by </title>
  <author>
    <name></name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://yabu.me/npub127u525u3l674p6l40c72u4ptxvumhfa0hu4pyjztnrw7vpppmvrqun5r64.rss" />
  <link href="https://yabu.me/npub127u525u3l674p6l40c72u4ptxvumhfa0hu4pyjztnrw7vpppmvrqun5r64" />
  <id>https://yabu.me/npub127u525u3l674p6l40c72u4ptxvumhfa0hu4pyjztnrw7vpppmvrqun5r64</id>
  <icon></icon>
  <logo></logo>




  <entry>
    <id>https://yabu.me/nevent1qqsyfnkv6yugddxsu3rs4vdgv8ugcknc0rupqqejzm8nhx6e8f5036gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvp02gu0</id>
    
      <title type="html">{ &amp;#34;nodes&amp;#34;: { &amp;#34;flake-utils&amp;#34;: { &amp;#34;inputs&amp;#34;: { ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsyfnkv6yugddxsu3rs4vdgv8ugcknc0rupqqejzm8nhx6e8f5036gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvp02gu0" />
    <content type="html">
      {&lt;br/&gt;  &amp;#34;nodes&amp;#34;: {&lt;br/&gt;    &amp;#34;flake-utils&amp;#34;: {&lt;br/&gt;      &amp;#34;inputs&amp;#34;: {&lt;br/&gt;        &amp;#34;systems&amp;#34;: &amp;#34;systems&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1731533236,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-l0KFg5HjrsfsO/JpG&#43;r7fRrqm12kzFHyUHqHCVpMMbI=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;numtide&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;flake-utils&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;11707dc2f618dd54ca8739b309ec4fc024de578b&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;numtide&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;flake-utils&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;nixpkgs&amp;#34;: {&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1754498491,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;c2ae88e026f9525daf89587f3cbee584b92b6134&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;ref&amp;#34;: &amp;#34;nixos-unstable&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;nixpkgs_2&amp;#34;: {&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1744536153,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;18dd725c29603f582cf1900e0d25f9f1063dbf11&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;NixOS&amp;#34;,&lt;br/&gt;        &amp;#34;ref&amp;#34;: &amp;#34;nixpkgs-unstable&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;root&amp;#34;: {&lt;br/&gt;      &amp;#34;inputs&amp;#34;: {&lt;br/&gt;        &amp;#34;flake-utils&amp;#34;: &amp;#34;flake-utils&amp;#34;,&lt;br/&gt;        &amp;#34;nixpkgs&amp;#34;: &amp;#34;nixpkgs&amp;#34;,&lt;br/&gt;        &amp;#34;rust-overlay&amp;#34;: &amp;#34;rust-overlay&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;rust-overlay&amp;#34;: {&lt;br/&gt;      &amp;#34;inputs&amp;#34;: {&lt;br/&gt;        &amp;#34;nixpkgs&amp;#34;: &amp;#34;nixpkgs_2&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1754575663,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;oxalica&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;rust-overlay&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;oxalica&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;rust-overlay&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;systems&amp;#34;: {&lt;br/&gt;      &amp;#34;locked&amp;#34;: {&lt;br/&gt;        &amp;#34;lastModified&amp;#34;: 1681028828,&lt;br/&gt;        &amp;#34;narHash&amp;#34;: &amp;#34;sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=&amp;#34;,&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;nix-systems&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;default&amp;#34;,&lt;br/&gt;        &amp;#34;rev&amp;#34;: &amp;#34;da67096a3b9bf56a91d16901293e51ba5b49a27e&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;original&amp;#34;: {&lt;br/&gt;        &amp;#34;owner&amp;#34;: &amp;#34;nix-systems&amp;#34;,&lt;br/&gt;        &amp;#34;repo&amp;#34;: &amp;#34;default&amp;#34;,&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;github&amp;#34;&lt;br/&gt;      }&lt;br/&gt;    }&lt;br/&gt;  },&lt;br/&gt;  &amp;#34;root&amp;#34;: &amp;#34;root&amp;#34;,&lt;br/&gt;  &amp;#34;version&amp;#34;: 7&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:37:44Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsvuf7xc4j5v5wq9f774qh4ul779e4n83w7r4m9vf23gmv0yvt6m4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvf4xk4d</id>
    
      <title type="html"># Fetch a Patch By ID &amp;gt; `n34 patch fetch` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsvuf7xc4j5v5wq9f774qh4ul779e4n83w7r4m9vf23gmv0yvt6m4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvf4xk4d" />
    <content type="html">
      # Fetch a Patch By ID&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch fetch` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Fetches a patch by its id&lt;br/&gt;&lt;br/&gt;Usage: n34 patch fetch [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The patch id to fetch it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -o, --output &amp;lt;PATH&amp;gt;              Output directory for the patches. Default to the current directory&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Fetches patches using their original patch ID. All fetched patches will be saved&lt;br/&gt;to the specified output directory (current directory by default). You can then&lt;br/&gt;apply or merge these patches into your branch as needed.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:37:38Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqst9yny86mmc088y3th00qquqgtwmv6a0uwrd2kgfqc8ve3n8l6wuszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfrksv3</id>
    
      <title type="html"># Modify a Set &amp;gt; `n34 sets update` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqst9yny86mmc088y3th00qquqgtwmv6a0uwrd2kgfqc8ve3n8l6wuszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfrksv3" />
    <content type="html">
      # Modify a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets update` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Modify an existing set&lt;br/&gt;&lt;br/&gt;Usage: n34 sets update [OPTIONS] &amp;lt;NAME&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;NAME&amp;gt;  Name of the set to update&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --set-relay &amp;lt;RELAYS&amp;gt;         Add relay to the set, either as URL or set name to extract its relays. [aliases: `--sr`]&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --override                   Replace existing relays/repositories instead of adding to them&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Use this command to update an existing set by its name. By default, providing&lt;br/&gt;relays via `--set-relay` or repositories via `--repo` will add them to the set&amp;#39;s&lt;br/&gt;existing entries. To replace the current relays and repositories with the new&lt;br/&gt;values, use the `--override` flag.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:37:31Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqswz4ck4g0e35ur090fksqwe84cynhg464g43ymz4crtwv2jfhv4tgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvy92guw</id>
    
      <title type="html"># Draft an Open Patch &amp;gt; `n34 patch draft` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqswz4ck4g0e35ur090fksqwe84cynhg464g43ymz4crtwv2jfhv4tgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvy92guw" />
    <content type="html">
      # Draft an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch draft` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Converts an open patch to draft state&lt;br/&gt;&lt;br/&gt;Usage: n34 patch draft [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The open patch id to draft it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1633` (Draft status) for the specified patch. The patch have to&lt;br/&gt;be open.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:37:21Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsgadu0z0d6n6asc89wspa5uk885qedwglq5qhuxgu5w25q6zxasfszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2zf0dw</id>
    
      <title type="html"># Show a Set &amp;gt; `n34 sets show` command **Usage:** ``` Show a ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsgadu0z0d6n6asc89wspa5uk885qedwglq5qhuxgu5w25q6zxasfszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2zf0dw" />
    <content type="html">
      # Show a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets show` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Show a single set or all the stored sets&lt;br/&gt;&lt;br/&gt;Usage: n34 sets show [NAME]&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NAME]  Name of the set to display. If not provided, lists all available sets&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Prints all set names to standard output, each followed by its associated repos&lt;br/&gt;and relays. To view the details for a specific set, provide its name as an&lt;br/&gt;argument.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:37:11Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxvfaeca4gk6d00ys3kpx74n0ckvwf6cjgnysms6qgalq9xkznzrgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvwjckrt</id>
    
      <title type="html"># Closes an Open or Drafted Patch &amp;gt; `n34 patch close` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxvfaeca4gk6d00ys3kpx74n0ckvwf6cjgnysms6qgalq9xkznzrgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvwjckrt" />
    <content type="html">
      # Closes an Open or Drafted Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch close` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Closes an open or drafted patch&lt;br/&gt;&lt;br/&gt;Usage: n34 patch close [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The open/drafted patch id to close it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified patch. The patch have to&lt;br/&gt;be open or drafted.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:37:07Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqspjpxzfqq23tkh4egtv9hs2nqpg0servnpytpp7cgp3mrhhvf85nczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3jscxr</id>
    
      <title type="html"># Remove a Set &amp;gt; `n34 sets remove` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqspjpxzfqq23tkh4egtv9hs2nqpg0servnpytpp7cgp3mrhhvf85nczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3jscxr" />
    <content type="html">
      # Remove a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets remove` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Remove a set, or specific repos and relays within it&lt;br/&gt;&lt;br/&gt;Usage: n34 sets remove [OPTIONS] &amp;lt;NAME&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;NAME&amp;gt;  Set name to delete&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --set-relay &amp;lt;RELAYS&amp;gt;         Specific relay to remove it from the set, either as URL or set name to extract its relays. [aliases: `--sr`]&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Removes an entire set, or specific repositories and relays from it.&lt;br/&gt;Without options, this command deletes the entire set.&lt;br/&gt;&lt;br/&gt;See the [passing repositories] section for more details on supported formats.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:58Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs9w685lyfcs7uemlzdkrdyz9vzeq5w2raqapcylepdnn60zjr7v6gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvzc4y00</id>
    
      <title type="html"># Apply an Open Patch &amp;gt; `n34 patch apply` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs9w685lyfcs7uemlzdkrdyz9vzeq5w2raqapcylepdnn60zjr7v6gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvzc4y00" />
    <content type="html">
      # Apply an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch apply` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Set an open patch status to applied&lt;br/&gt;&lt;br/&gt;Usage: n34 patch apply [OPTIONS] &amp;lt;PATCH_ID&amp;gt; [APPLIED_COMMITS]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;            The open patch id to apply it. Must be orignal root patch or revision root&lt;br/&gt;  [APPLIED_COMMITS]...  The applied commits&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --patches &amp;lt;PATCH-EVENT-ID&amp;gt;   Patches that have been applied. Use this when only some patches have been applied, not all&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Creates a kind `1631` event (Applied/Merged status) for the specified patch. The&lt;br/&gt;patch must be in open status.&lt;br/&gt;&lt;br/&gt;You can specify either an original patch or revision patch ID, but the status&lt;br/&gt;event will only reference the original patch. Revision patches will be mentioned&lt;br/&gt;in the event.&lt;br/&gt;&lt;br/&gt;The `APPLIED_COMMITS` field serves to inform clients about the status of&lt;br/&gt;specific commits, whether they have been applied or not. If you need to retrieve&lt;br/&gt;the list of commits from a specific point (such as the tip of the master branch)&lt;br/&gt;up to the `HEAD`, you can use the following Git command: `git log --pretty=%H&lt;br/&gt;&amp;#39;origin/master..HEAD&amp;#39;`.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:56Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsrmch6jqjgcque66k60h4kxqc2stjru26lggq4srnxx67x04fduaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvg9gw4</id>
    
      <title type="html"># Create a Set &amp;gt; `n34 sets new` command **Usage:** ``` Create ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsrmch6jqjgcque66k60h4kxqc2stjru26lggq4srnxx67x04fduaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvg9gw4" />
    <content type="html">
      # Create a Set&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 sets new` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Create a new set&lt;br/&gt;&lt;br/&gt;Usage: n34 sets new [OPTIONS] &amp;lt;NAME&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;NAME&amp;gt;  Unique name for the set&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --set-relay &amp;lt;RELAYS&amp;gt;         Optional relay to add it to the set, either as URL or set name to extract its relays. [aliases: `--sr`]&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Each set requires a unique name, provided as the final argument to the command.&lt;br/&gt;Use the `--set-relays`/`--sr` option to specify the relays for the new set;&lt;br/&gt;this can be a relay URL or the name of an existing set whose relays you wish to&lt;br/&gt;use. To add repositories, use the `--repo` option. Check [passing repositories]&lt;br/&gt;format.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:46Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqste6a2dyhzuvcerr0hf6522gvekcmqm6uhjetcxg00lp0h2vy7ccgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8tlpr9</id>
    
      <title type="html"># Patch Management In `n34`, patch management is designed to give ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqste6a2dyhzuvcerr0hf6522gvekcmqm6uhjetcxg00lp0h2vy7ccgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8tlpr9" />
    <content type="html">
      # Patch Management&lt;br/&gt;&lt;br/&gt;In `n34`, patch management is designed to give you complete control. You can&lt;br/&gt;manually generate patch files using `git-format-patch` and then broadcast them&lt;br/&gt;to Nostr relays. This ensures that you have full authority over the content&lt;br/&gt;and structure of your patches, allowing for precise customization as per your&lt;br/&gt;requirements.&lt;br/&gt;&lt;br/&gt;Similarly, when fetching patches, `n34` provides them to you without&lt;br/&gt;automatically applying, merging, or checking them. This empowers you to review&lt;br/&gt;the patches at your own pace and decide whether to merge or apply them as&lt;br/&gt;needed. You retain full control over the entire process, ensuring a tailored&lt;br/&gt;approach to patch management.&lt;br/&gt;&lt;br/&gt;## Patch Status Management&lt;br/&gt;&lt;br/&gt;You can assign a status to original patches, but revision patches do not have&lt;br/&gt;a specific status assigned to them. Instead, they inherit the status of the&lt;br/&gt;original patch. However, if the original patch is marked as `Applied/Merged`,&lt;br/&gt;the revision patch must be explicitly tagged to claim the same status. If not&lt;br/&gt;tagged, the revision patch status will be `Closed`.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:40Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqspu78y2hxaysh2xvxk53qkpng59dhp99wtpjc2wdffss8nnenvergzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvpy6mdv</id>
    
      <title type="html"># Managing Repository and Relay Sets Sets are a convenience ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqspu78y2hxaysh2xvxk53qkpng59dhp99wtpjc2wdffss8nnenvergzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvpy6mdv" />
    <content type="html">
      # Managing Repository and Relay Sets&lt;br/&gt;&lt;br/&gt;Sets are a convenience feature for contributing to projects that do not have a&lt;br/&gt;`nostr-address` file. Instead of manually specifying the project&amp;#39;s repositories&lt;br/&gt;and relays for every command, you can define them once as a named &amp;#34;set&amp;#34;. You can&lt;br/&gt;then reference this set by its name in commands. This allows you to use the set&lt;br/&gt;as a shortcut for a list of relays (`--relays &amp;lt;set_name&amp;gt;`) or as the project&amp;#39;s&lt;br/&gt;address in commands like `issue` and `patch`.&lt;br/&gt;&lt;br/&gt;Sets are defined in your configuration file. To use a specific configuration&lt;br/&gt;file, pass its path using the `--config` option.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:34Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsdrwaq9zed2f2h6eqjacmwpqll7yx6fh7zd76k6uknn2mah9mm4tgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvn4k25x</id>
    
      <title type="html"># View an Issue By ID &amp;gt; `n34 issue view` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsdrwaq9zed2f2h6eqjacmwpqll7yx6fh7zd76k6uknn2mah9mm4tgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvn4k25x" />
    <content type="html">
      # View an Issue By ID&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue view` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;View an issue by its ID&lt;br/&gt;&lt;br/&gt;Usage: n34 issue view [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The issue id to view it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Simply provide the issue ID in `note` or `nevent` format to retrieve and display&lt;br/&gt;the issue details.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:29Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsfe8cmpa3aw49qtyneggq8p2ad9hjdfq5j7uu8pd43e3zd4nw24eszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsva525wr</id>
    
      <title type="html"># View Git Repository Details &amp;gt; `n34 repo view` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsfe8cmpa3aw49qtyneggq8p2ad9hjdfq5j7uu8pd43e3zd4nw24eszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsva525wr" />
    <content type="html">
      # View Git Repository Details&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 repo view` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;View details of a nostr git repository&lt;br/&gt;&lt;br/&gt;Usage: n34 repo view [NADDR-NIP05-OR-SET]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NADDR-NIP05-OR-SET]...  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command prints repository details to standard output. If no arguments&lt;br/&gt;are provided, it looks for a `nostr-address` file in the current directory&lt;br/&gt;and displays the details for the address specified within it. See [passing&lt;br/&gt;repositories] for details on accepted formats.&lt;br/&gt;&lt;br/&gt;[passing repositories]: /commands.html#passing-repositories&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:23Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs8zsm4esjj2s3euqvwuzveywa29vxzy27ut9eu5pz3g36eqdf7kjczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrqkp9j</id>
    
      <title type="html"># Resolves an Issue &amp;gt; `n34 issue resolve` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs8zsm4esjj2s3euqvwuzveywa29vxzy27ut9eu5pz3g36eqdf7kjczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrqkp9j" />
    <content type="html">
      # Resolves an Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue resolve` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Resolves an issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue resolve [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The issue id to resolve it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1631` (Resolved status) event for the specified issue.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:17Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsrrk6dw0k3eugjz6fhvyghntuz98nrdx7ewclxmu5c96g2sz2nykczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9efc80</id>
    
      <title type="html"># Broadcast and Update a Git Repository &amp;gt; `n34 repo announce` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsrrk6dw0k3eugjz6fhvyghntuz98nrdx7ewclxmu5c96g2sz2nykczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9efc80" />
    <content type="html">
      # Broadcast and Update a Git Repository&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 repo announce` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Broadcast and update a git repository&lt;br/&gt;&lt;br/&gt;Usage: n34 repo announce [OPTIONS] --id &amp;lt;REPO_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --id &amp;lt;REPO_ID&amp;gt;               Unique identifier for the repository in kebab-case&lt;br/&gt;  -n, --name &amp;lt;NAME&amp;gt;                A name for the repository&lt;br/&gt;  -d, --description &amp;lt;DESCRIPTION&amp;gt;  A description for the repository&lt;br/&gt;  -w, --web &amp;lt;WEB&amp;gt;                  Webpage URLs for the repository (if provided by the git server)&lt;br/&gt;  -c, --clone &amp;lt;CLONE&amp;gt;              URLs for cloning the repository&lt;br/&gt;  -m, --maintainers &amp;lt;MAINTAINERS&amp;gt;  Additional maintainers of the repository (besides yourself)&lt;br/&gt;  -l, --label &amp;lt;LABEL&amp;gt;              Labels to categorize the repository. Can be specified multiple times&lt;br/&gt;      --force-id                   Skip kebab-case validation for the repository ID&lt;br/&gt;      --address-file               If set, creates a `nostr-address` file to enable automatic address discovery by n34&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command generates an announcement event to publish your project. It can be&lt;br/&gt;used to announce a new repository or update an existing one.&lt;br/&gt;&lt;br/&gt;When updating, you must resubmit all repository fields, not just the fields&lt;br/&gt;you wish to change. The command uses this information to build and publish a&lt;br/&gt;completely new announcement event that will replace the old one.&lt;br/&gt;&lt;br/&gt;It is recommended to use the `--address-file` flag. This option creates&lt;br/&gt;a `nostr-address` file that enables `n34` to automatically discover the&lt;br/&gt;repository&amp;#39;s address, simplifying the workflow for contributors.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:36:12Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs0uevta3q4tajh2ux7w6n25urnekxhll0lh9km3jzw92kewwn8xaczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvlduc7p</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs0uevta3q4tajh2ux7w6n25urnekxhll0lh9km3jzw92kewwn8xaczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvlduc7p" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::SocketAddr, /*process::ExitCode, */sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::Extension;&lt;br/&gt;use hyper::{Method, header};&lt;br/&gt;use tokio::signal;&lt;br/&gt;use tower_http::{&lt;br/&gt;    cors,&lt;br/&gt;    decompression::RequestDecompressionLayer,&lt;br/&gt;    trace::{DefaultMakeSpan, TraceLayer},&lt;br/&gt;};&lt;br/&gt;use tracing::level_filters::LevelFilter;&lt;br/&gt;use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;/// Relay endpoints&lt;br/&gt;pub mod endpoints;&lt;br/&gt;/// Relay errors.&lt;br/&gt;pub mod errors;&lt;br/&gt;/// Extension traits&lt;br/&gt;pub mod ext_traits;&lt;br/&gt;/// GRASP git server&lt;br/&gt;pub mod git_server;&lt;br/&gt;/// Relay pathes.&lt;br/&gt;pub mod pathes;&lt;br/&gt;/// Raw axum websocket&lt;br/&gt;pub mod raw_websocket;&lt;br/&gt;/// Our relay.&lt;br/&gt;pub mod relay;&lt;br/&gt;/// Relay configuration.&lt;br/&gt;pub mod relay_config;&lt;br/&gt;/// Router state&lt;br/&gt;pub mod router_state;&lt;br/&gt;/// Some useful utils.&lt;br/&gt;pub mod utils;&lt;br/&gt;&lt;br/&gt;use self::{errors::RelayResult, relay_config::RelayConfig, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Sets up default logging with two outputs, stderr and a log file.&lt;br/&gt;///&lt;br/&gt;/// Log level for stderr is controlled by `RUST_LOG` environment variable,&lt;br/&gt;/// defaults to `ERROR`. The log file always uses `TRACE` level.&lt;br/&gt;pub fn setup_logs() -&amp;gt; errors::RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    tracing::subscriber::set_global_default(&lt;br/&gt;        tracing_subscriber::registry()&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(true)&lt;br/&gt;                    .with_writer(std::io::stderr)&lt;br/&gt;                    .without_time()&lt;br/&gt;                    .with_filter(EnvFilter::from_default_env()),&lt;br/&gt;            )&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(false)&lt;br/&gt;                    .with_writer(utils::logs_file()?)&lt;br/&gt;                    .with_file(false)&lt;br/&gt;                    .with_line_number(false)&lt;br/&gt;                    .with_filter(LevelFilter::TRACE),&lt;br/&gt;            ),&lt;br/&gt;    )&lt;br/&gt;    .ok();&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub async fn shutdown_signal() {&lt;br/&gt;    let ctrl_c = async {&lt;br/&gt;        signal::ctrl_c()&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;Failed to install CTRL&#43;C handler&amp;#34;);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(unix)]&lt;br/&gt;    let terminate = async {&lt;br/&gt;        use tokio::signal::unix::SignalKind;&lt;br/&gt;&lt;br/&gt;        signal::unix::signal(SignalKind::terminate())&lt;br/&gt;            .expect(&amp;#34;Failed to create SIGTERM handler&amp;#34;)&lt;br/&gt;            .recv()&lt;br/&gt;            .await;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(not(unix))]&lt;br/&gt;    let terminate = std::future::pending::&amp;lt;()&amp;gt;();&lt;br/&gt;&lt;br/&gt;    tokio::select! {&lt;br/&gt;        _ = ctrl_c =&amp;gt; {},&lt;br/&gt;        _ = terminate =&amp;gt; {},&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub async fn try_main() -&amp;gt; RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    setup_logs()?;&lt;br/&gt;    let config = Arc::new(RelayConfig::reload()?);&lt;br/&gt;    let relay_db = config.get_relay_db().await?;&lt;br/&gt;    let n34_relay = Arc::new(relay::build_relay(Arc::clone(&amp;amp;config), Arc::clone(&amp;amp;relay_db)).await);&lt;br/&gt;    let addr = SocketAddr::new(config.net.ip, config.net.port);&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&amp;#34;Running relay with configuration: {config:#?}&amp;#34;);&lt;br/&gt;    tracing::info!(&amp;#34;Relay is running at `{}`&amp;#34;, n34_relay.url().await);&lt;br/&gt;&lt;br/&gt;    let mut app = axum::Router::new()&lt;br/&gt;        // main handler. GET and POST&lt;br/&gt;        .route(&amp;#34;/&amp;#34;, axum::routing::get(endpoints::main_handler).post(endpoints::main_handler));&lt;br/&gt;&lt;br/&gt;    if config.grasp.enable {&lt;br/&gt;        tracing::info!(&amp;#34;Git server is running&amp;#34;);&lt;br/&gt;        app = app.merge(git_server::router(&amp;amp;config));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    app = app&lt;br/&gt;        // enable cross-origin access&lt;br/&gt;        .route(&lt;br/&gt;            &amp;#34;/&amp;#34;,&lt;br/&gt;            axum::routing::options(|| async { hyper::StatusCode::NO_CONTENT }),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            cors::CorsLayer::new()&lt;br/&gt;                .allow_origin(cors::Any)&lt;br/&gt;                .allow_methods([Method::GET, Method::POST])&lt;br/&gt;                .allow_headers([header::CONTENT_TYPE]),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            TraceLayer::new_for_http()&lt;br/&gt;                .make_span_with(DefaultMakeSpan::default().include_headers(true)),&lt;br/&gt;        )&lt;br/&gt;        .layer(RequestDecompressionLayer::new())&lt;br/&gt;        .layer(Extension(Arc::new(RouterState::new(&lt;br/&gt;            config, n34_relay, relay_db,&lt;br/&gt;        ))));&lt;br/&gt;&lt;br/&gt;    axum::serve(&lt;br/&gt;        tokio::net::TcpListener::bind(addr).await?,&lt;br/&gt;        app.into_make_service_with_connect_info::&amp;lt;SocketAddr&amp;gt;(),&lt;br/&gt;    )&lt;br/&gt;    .with_graceful_shutdown(shutdown_signal())&lt;br/&gt;    .await?;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// #[tokio::main]&lt;br/&gt;// async fn main() -&amp;gt; ExitCode {&lt;br/&gt;//     if let Err(err) = try_main().await {&lt;br/&gt;//         eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;//         return ExitCode::FAILURE;&lt;br/&gt;//     }&lt;br/&gt;// &lt;br/&gt;//     tracing::info!(&amp;#34;Exited gracefully without any errors&amp;#34;);&lt;br/&gt;//     ExitCode::SUCCESS&lt;br/&gt;// }&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:24:06Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsqusj4naf6lwc9zyyzkdrhhnwyc9zj43rf9lxn7dkl9snchwkrdfqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvcp4klf</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsqusj4naf6lwc9zyyzkdrhhnwyc9zj43rf9lxn7dkl9snchwkrdfqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvcp4klf" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use nostr_database::NostrDatabase;&lt;br/&gt;use nostr_relay_builder::{&lt;br/&gt;    LocalRelay,&lt;br/&gt;&lt;br/&gt;    builder::{LocalRelayBuilderNip42, RateLimit},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;pub use self::{grpc_manager::GrpcError, rhai_manager::RhaiPluginsError};&lt;br/&gt;use crate::relay_config::RelayConfig;&lt;br/&gt;&lt;br/&gt;/// A plugin to check the event size.&lt;br/&gt;mod event_size_plugin;&lt;br/&gt;/// Core GRASP (Git Relays Authorized via Signed-Nostr Proofs) implementation&lt;br/&gt;mod grasp;&lt;br/&gt;/// gRPC plugins manager&lt;br/&gt;mod grpc_manager;&lt;br/&gt;/// Plugins manager&lt;br/&gt;mod plugins_manager;&lt;br/&gt;/// Rhai plugins manager&lt;br/&gt;mod rhai_manager;&lt;br/&gt;/// Plugins that manages a whitelist and blacklist&lt;br/&gt;mod whitelist_plugins;&lt;br/&gt;&lt;br/&gt;/// Middlewares state&lt;br/&gt;pub struct MiddlewareState {&lt;br/&gt;    /// The relay config&lt;br/&gt;    pub config: Arc&amp;lt;RelayConfig&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl MiddlewareState {&lt;br/&gt;    /// Construct a new middleware state&lt;br/&gt;    pub fn new(config: Arc&amp;lt;RelayConfig&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        Self { config }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Build a relay from the config&lt;br/&gt;pub async fn build_relay(config: Arc&amp;lt;RelayConfig&amp;gt;, relay_db: Arc&amp;lt;dyn NostrDatabase&amp;gt;) -&amp;gt; LocalRelay {&lt;br/&gt;    let mut plugins_builder = plugins_manager::PluginsManagerBuilder::with_middlewares_state(&lt;br/&gt;        Arc::new(MiddlewareState::new(Arc::clone(&amp;amp;config))),&lt;br/&gt;    )&lt;br/&gt;    .add_all(event_size_plugin::EventSizePlugin(&lt;br/&gt;        config.relay.max_event_size.into(),&lt;br/&gt;    ))&lt;br/&gt;    .add_all(whitelist_plugins::PubKeyBlacklist(Arc::clone(&lt;br/&gt;        &amp;amp;config.relay.blacklist,&lt;br/&gt;    )))&lt;br/&gt;    .add_all(whitelist_plugins::KindBlacklist(Arc::clone(&lt;br/&gt;        &amp;amp;config.relay.disallowed_kinds,&lt;br/&gt;    )))&lt;br/&gt;    .add_all(whitelist_plugins::KindWhitelist(Arc::clone(&lt;br/&gt;        &amp;amp;config.relay.allowed_kinds,&lt;br/&gt;    )))&lt;br/&gt;    .add_any(whitelist_plugins::PubKeyWhiteList(Arc::clone(&lt;br/&gt;        &amp;amp;config.relay.whitelist,&lt;br/&gt;    )))&lt;br/&gt;    .add_any(whitelist_plugins::MentionedPubKey(Arc::clone(&lt;br/&gt;        &amp;amp;config.relay.whitelist,&lt;br/&gt;    )))&lt;br/&gt;    .add_plugins_manager(grpc_manager::GrpcPluginsManager::maybe_manager(&amp;amp;config).await)&lt;br/&gt;    .add_plugins_manager(rhai_manager::maybe_manager(&amp;amp;config).await);&lt;br/&gt;&lt;br/&gt;    if config.grasp.enable {&lt;br/&gt;        plugins_builder = plugins_builder&lt;br/&gt;            .add_all(grasp::plugins::ValidateRepoState)&lt;br/&gt;            .add_all(grasp::plugins::ValidateRepoEvent)&lt;br/&gt;            .add_all(grasp::plugins::RejectRepoState::new(Arc::clone(&amp;amp;relay_db)))&lt;br/&gt;            .add_all(grasp::plugins::GraspRepo::new(&amp;amp;config.relay.domain))&lt;br/&gt;            .add_any(grasp::plugins::AcceptMention::new(Arc::clone(&amp;amp;relay_db)))&lt;br/&gt;            .add_middleware(grasp::middlewares::repo_creator);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let plugins = plugins_builder.build();&lt;br/&gt;    let mut relay_builder = LocalRelay::builder()&lt;br/&gt;        .addr(config.net.ip)&lt;br/&gt;        .port(config.net.port)&lt;br/&gt;        .auth_dm(true)&lt;br/&gt;        .min_pow(config.relay.min_pow)&lt;br/&gt;        .rate_limit(RateLimit::from(config.ratelimit.clone()))&lt;br/&gt;        .max_filter_limit(config.relay.max_limit.into())&lt;br/&gt;        .default_filter_limit(config.relay.default_limit.into())&lt;br/&gt;        .max_subid_length(config.relay.max_subid_length.into())&lt;br/&gt;        .database(relay_db)&lt;br/&gt;        .write_policy(plugins.clone())&lt;br/&gt;        .query_policy(plugins);&lt;br/&gt;&lt;br/&gt;    if config.relay.nip42 {&lt;br/&gt;        relay_builder = relay_builder.nip42(LocalRelayBuilderNip42::read_and_write());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if let Some(max_connections) = config.relay.max_connections {&lt;br/&gt;        relay_builder = relay_builder.max_connections(max_connections)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    relay_builder.build()&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:24:04Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs8aa6fkmwkuw8aeu7gqg2cktyupsg7aarlqrdf6fh3g5lnhr3nhmgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgkzem7</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs8aa6fkmwkuw8aeu7gqg2cktyupsg7aarlqrdf6fh3g5lnhr3nhmgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgkzem7" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{collections::HashMap, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::{body::Body, response::Response};&lt;br/&gt;use hyper::{HeaderMap, StatusCode};&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, Kind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;};&lt;br/&gt;use nostr_database::NostrDatabase;&lt;br/&gt;&lt;br/&gt;use crate::git_server::{PublicKeyAndRepoPath, ref_update_pkt_line::RefUpdatePkt};&lt;br/&gt;&lt;br/&gt;/// unpack-status = PKT-LINE(&amp;#34;unpack&amp;#34; SP unpack-result)&lt;br/&gt;/// unpack-result = &amp;#34;ok&amp;#34; / error-msg&lt;br/&gt;const UNPACK_OK: &amp;amp;[u8] = b&amp;#34;unpack ok&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Checks if the provided headers contain the `Git-Protocol: version=2` header.&lt;br/&gt;pub fn contains_git_v2(headers: &amp;amp;HeaderMap) -&amp;gt; bool {&lt;br/&gt;    headers&lt;br/&gt;        .get(&amp;#34;Git-Protocol&amp;#34;)&lt;br/&gt;        .is_some_and(|value| value == &amp;#34;version=2&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Add the data length before it. `{length}{data}`&lt;br/&gt;fn pkt_line(data: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let mut result = format!(&amp;#34;{:04x}&amp;#34;, data.len() &#43; 4).into_bytes();&lt;br/&gt;    result.extend_from_slice(data);&lt;br/&gt;    result&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Make a pkt_line with a side-band. `{length}{channel}pkt_line({data})`&lt;br/&gt;fn sideband_pkt_line(channel: u8, data: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let pktline = if data == b&amp;#34;0000&amp;#34; {&lt;br/&gt;        data&lt;br/&gt;    } else {&lt;br/&gt;        &amp;amp;pkt_line(data)&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    // 4 bytes for length &#43; 1 byte for channel &#43; pktline&lt;br/&gt;    let mut result = format!(&amp;#34;{:04x}&amp;#34;, pktline.len() &#43; 5).into_bytes();&lt;br/&gt;    result.push(channel);&lt;br/&gt;    result.extend_from_slice(pktline);&lt;br/&gt;    result&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Formats an error message using Git&amp;#39;s pkt-line report-status.&lt;br/&gt;fn git_errors_response(&lt;br/&gt;    refs_errors: &amp;amp;HashMap&amp;lt;&amp;amp;str, &amp;amp;&amp;#39;static str&amp;gt;,&lt;br/&gt;    all_refs: &amp;amp;[RefUpdatePkt],&lt;br/&gt;    capabilities: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let mut response = Vec::new();&lt;br/&gt;    let caps_contains = |cap| capabilities.split(&amp;#34; &amp;#34;).any(|c| c == cap);&lt;br/&gt;    let use_sideband = caps_contains(&amp;#34;side-band-64k&amp;#34;) || caps_contains(&amp;#34;side-band&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // report-status = unpack-status&lt;br/&gt;    //                 1*(command-status)&lt;br/&gt;    //                 flush-pkt&lt;br/&gt;    if caps_contains(&amp;#34;report-status&amp;#34;) || caps_contains(&amp;#34;report-status-v2&amp;#34;) {&lt;br/&gt;        // command-status = command-ok / command-fail&lt;br/&gt;        // command-ok     = PKT-LINE(&amp;#34;ok&amp;#34; SP refname)&lt;br/&gt;        // command-fail   = PKT-LINE(&amp;#34;ng&amp;#34; SP refname SP error-msg)&lt;br/&gt;        let ng_line = |ref_name, msg| format!(&amp;#34;ng {ref_name} {msg}&amp;#34;);&lt;br/&gt;        let ok_line = |ref_name| format!(&amp;#34;ok {ref_name}&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if use_sideband {&lt;br/&gt;            response.extend_from_slice(&amp;amp;sideband_pkt_line(1, UNPACK_OK));&lt;br/&gt;            for ref_update in all_refs {&lt;br/&gt;                if let Some(err) = refs_errors.get(ref_update.ref_name) {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;sideband_pkt_line(&lt;br/&gt;                        1,&lt;br/&gt;                        ng_line(&amp;amp;ref_update.ref_name, err).as_bytes(),&lt;br/&gt;                    ));&lt;br/&gt;                } else {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;sideband_pkt_line(&lt;br/&gt;                        1,&lt;br/&gt;                        ok_line(ref_update.ref_name).as_bytes(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            // side-band flush-pkt&lt;br/&gt;            response.extend_from_slice(&amp;amp;sideband_pkt_line(1, b&amp;#34;0000&amp;#34;));&lt;br/&gt;            // flush-pkt&lt;br/&gt;            response.extend_from_slice(b&amp;#34;0000&amp;#34;);&lt;br/&gt;        } else {&lt;br/&gt;            response.extend_from_slice(&amp;amp;pkt_line(UNPACK_OK));&lt;br/&gt;            for ref_update in all_refs {&lt;br/&gt;                if let Some(err) = refs_errors.get(ref_update.ref_name) {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;pkt_line(&lt;br/&gt;                        ng_line(&amp;amp;ref_update.ref_name, err).as_bytes(),&lt;br/&gt;                    ));&lt;br/&gt;                } else {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;pkt_line(ok_line(ref_update.ref_name).as_bytes()));&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            // flush-pkt&lt;br/&gt;            response.extend_from_slice(b&amp;#34;0000&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;    response&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates an error response that Git clients will display to users.&lt;br/&gt;pub fn git_receive_pack_error(&lt;br/&gt;    refs_errors: &amp;amp;HashMap&amp;lt;&amp;amp;str, &amp;amp;&amp;#39;static str&amp;gt;,&lt;br/&gt;    all_refs: &amp;amp;[RefUpdatePkt],&lt;br/&gt;    capabilities: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let body = git_errors_response(refs_errors, all_refs, capabilities);&lt;br/&gt;&lt;br/&gt;    Response::builder()&lt;br/&gt;        .status(StatusCode::OK)&lt;br/&gt;        .header(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/x-git-receive-pack-result&amp;#34;)&lt;br/&gt;        .body(Body::from(body))&lt;br/&gt;        .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extract the refs from the repository state event&lt;br/&gt;#[inline]&lt;br/&gt;pub fn extract_refs(repo_state: &amp;amp;Event) -&amp;gt; impl Iterator&amp;lt;Item = (&amp;amp;str, &amp;amp;str)&amp;gt; {&lt;br/&gt;    repo_state.tags.iter().filter_map(|tag| {&lt;br/&gt;        let tag_kind = tag.as_slice().first()?.as_str();&lt;br/&gt;        if (tag_kind.starts_with(&amp;#34;refs/heads/&amp;#34;) || tag_kind.starts_with(&amp;#34;refs/tags/&amp;#34;))&lt;br/&gt;            &amp;amp;&amp;amp; let Some(commit_id) = tag.content()&lt;br/&gt;        {&lt;br/&gt;            return Some((tag_kind, commit_id));&lt;br/&gt;        }&lt;br/&gt;        None&lt;br/&gt;    })&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Check if the ref match with the repository state&lt;br/&gt;pub fn check_ref_update(ref_update: &amp;amp;RefUpdatePkt, repo_state: &amp;amp;Event) -&amp;gt; Result&amp;lt;(), &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    let ref_commit = extract_refs(repo_state)&lt;br/&gt;        .find_map(|(name, commit)| (ref_update.ref_name == name).then_some(commit));&lt;br/&gt;&lt;br/&gt;    // Return an error if the ref is not found and the operation is not a delete&lt;br/&gt;    if ref_commit.is_none() &amp;amp;&amp;amp; !ref_update.is_delete() {&lt;br/&gt;        return Err(&lt;br/&gt;            &amp;#34;Reference not found. The reference must exist in the repository state to update it&amp;#34;,&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Return an error if attempting to delete a ref that is still present in the&lt;br/&gt;    // repository state&lt;br/&gt;    if ref_commit.is_some() &amp;amp;&amp;amp; ref_update.is_delete() {&lt;br/&gt;        return Err(&amp;#34;Cannot delete reference: it still exists in the repository state&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Return an error if the ref&amp;#39;s current commit doesn&amp;#39;t match the new commit&lt;br/&gt;    if let Some(commit) = ref_commit&lt;br/&gt;        &amp;amp;&amp;amp; commit != ref_update.new_commit&lt;br/&gt;    {&lt;br/&gt;        return Err(&lt;br/&gt;            &amp;#34;Commit mismatch: the new commit doesn&amp;#39;t match the repository state for this reference&amp;#34;,&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Accept if:&lt;br/&gt;    // - ref is not found the the commit is delete&lt;br/&gt;    // - ref is found and match the new commit&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Check if the push should be continue, if not return the error message&lt;br/&gt;pub async fn is_legal_push&amp;lt;&amp;#39;a&amp;gt;(&lt;br/&gt;    ref_updates: &amp;amp;[RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt;],&lt;br/&gt;    db: &amp;amp;Arc&amp;lt;dyn NostrDatabase&amp;gt;,&lt;br/&gt;    repo: &amp;amp;PublicKeyAndRepoPath,&lt;br/&gt;) -&amp;gt; Result&amp;lt;HashMap&amp;lt;&amp;amp;&amp;#39;a str, &amp;amp;&amp;#39;static str&amp;gt;, (StatusCode, &amp;amp;&amp;#39;static str)&amp;gt; {&lt;br/&gt;    // Repo announcement from the author&lt;br/&gt;    let Some(repo_announcement) = db&lt;br/&gt;        .query(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .author(repo.public_key)&lt;br/&gt;                .identifier(&amp;amp;repo.repo_name)&lt;br/&gt;                .kind(Kind::GitRepoAnnouncement),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .map_err(|_| {&lt;br/&gt;            (&lt;br/&gt;                StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;                &amp;#34;Database error. Try to push later :(&amp;#34;,&lt;br/&gt;            )&lt;br/&gt;        })?&lt;br/&gt;        .first_owned()&lt;br/&gt;    else {&lt;br/&gt;        return Err((&lt;br/&gt;            StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;            &amp;#34;No repository announcement found. It should be there but it&amp;#39;s not. Broadcast it \&lt;br/&gt;             please :)&amp;#34;,&lt;br/&gt;        ));&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    // Repo state event from the author or one of the maintainers&lt;br/&gt;    let Some(repo_state) = db&lt;br/&gt;        .query(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .author(repo.public_key)&lt;br/&gt;                .authors(crate::utils::get_maintainers(&amp;amp;repo_announcement).copied())&lt;br/&gt;                .identifier(&amp;amp;repo.repo_name)&lt;br/&gt;                .kind(Kind::RepoState),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .map_err(|_| {&lt;br/&gt;            (&lt;br/&gt;                StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;                &amp;#34;Database error. Try to push later :(&amp;#34;,&lt;br/&gt;            )&lt;br/&gt;        })?&lt;br/&gt;        .first_owned()&lt;br/&gt;    else {&lt;br/&gt;        return Err((&lt;br/&gt;            StatusCode::BAD_REQUEST,&lt;br/&gt;            &amp;#34;No repository state announcements found. Broadcast it to the relay first&amp;#34;,&lt;br/&gt;        ));&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    Ok(ref_updates&lt;br/&gt;        .iter()&lt;br/&gt;        .filter_map(|ref_update| {&lt;br/&gt;            check_ref_update(ref_update, &amp;amp;repo_state)&lt;br/&gt;                .err()&lt;br/&gt;                .map(|err| (ref_update.ref_name, err))&lt;br/&gt;        })&lt;br/&gt;        .collect())&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:55Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqszv82wvsga4qvz4lr4zekqvec4e35fxuu9n0aex8d9d90nut2e5ngzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvz9zxm3</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqszv82wvsga4qvz4lr4zekqvec4e35fxuu9n0aex8d9d90nut2e5ngzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvz9zxm3" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{collections::BTreeSet, sync::Arc};&lt;br/&gt;&lt;br/&gt;use either::Either;&lt;br/&gt;use nostr::{event::Event, filter::Filter, message::MachineReadablePrefix, util::BoxedFuture};&lt;br/&gt;use nostr_relay_builder::builder::{&lt;br/&gt;    QueryPolicy,&lt;br/&gt;    QueryPolicyResult,&lt;br/&gt;    WritePolicy,&lt;br/&gt;    WritePolicyResult,&lt;br/&gt;};&lt;br/&gt;use tonic::transport::Channel;&lt;br/&gt;&lt;br/&gt;use self::plugins_api::{&lt;br/&gt;    Empty,&lt;br/&gt;    PluginInfo,&lt;br/&gt;    PluginPriority,&lt;br/&gt;    PluginRequest,&lt;br/&gt;    PluginType,&lt;br/&gt;    plugin_request::PluginRequestBody,&lt;br/&gt;    plugin_response::PluginResponseBody,&lt;br/&gt;    plugins_service_client::PluginsServiceClient,&lt;br/&gt;};&lt;br/&gt;use crate::{relay::plugins_manager::PluginsManagerTrait, relay_config::RelayConfig};&lt;br/&gt;&lt;br/&gt;mod plugins_api {&lt;br/&gt;    tonic::include_proto!(&amp;#34;plugins&amp;#34;);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;type GrpcResult&amp;lt;T&amp;gt; = Result&amp;lt;T, GrpcError&amp;gt;;&lt;br/&gt;type PluginClient = PluginsServiceClient&amp;lt;Channel&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// gRPC errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum GrpcError {&lt;br/&gt;    #[error(&amp;#34;Transport: {0:?}&amp;#34;)]&lt;br/&gt;    Transport(#[from] tonic::transport::Error),&lt;br/&gt;    #[error(&amp;#34;Call error: {0}&amp;#34;)]&lt;br/&gt;    CallErr(String),&lt;br/&gt;    #[error(&amp;#34;A plugin service without any plugin {0}&amp;#34;)]&lt;br/&gt;    NoPlugins(hyper::Uri),&lt;br/&gt;    #[error(&amp;#34;Unknown plugin type: {0}&amp;#34;)]&lt;br/&gt;    UnknownPluginType(i32),&lt;br/&gt;    #[error(&amp;#34;Unknown plugin priority: {0}&amp;#34;)]&lt;br/&gt;    UnknownPluginPriority(i32),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Represents a gRPC plugin.&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct GrpcPlugin {&lt;br/&gt;    /// The name of the plugin.&lt;br/&gt;    name:     String,&lt;br/&gt;    /// Indicates whether the plugin is for writing or querying.&lt;br/&gt;    is_write: bool,&lt;br/&gt;    /// Plugin priority, `all` or `any`&lt;br/&gt;    is_all:   bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Handles gRPC plugin service.&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct GrpcPluginService {&lt;br/&gt;    /// Service uri&lt;br/&gt;    pub uri:               hyper::Uri,&lt;br/&gt;    /// The gRPC client used to call the `RunPlugin` RPC.&lt;br/&gt;    pub grpc_client:       PluginClient,&lt;br/&gt;    /// Write plugins of type `all`&lt;br/&gt;    pub all_write_plugins: Vec&amp;lt;GrpcPlugin&amp;gt;,&lt;br/&gt;    /// Write plugins of type `any`&lt;br/&gt;    pub any_write_plugins: Vec&amp;lt;GrpcPlugin&amp;gt;,&lt;br/&gt;    /// Query plugins of type `all`&lt;br/&gt;    pub all_query_plugins: Vec&amp;lt;GrpcPlugin&amp;gt;,&lt;br/&gt;    /// Query plugins of type `any`&lt;br/&gt;    pub any_query_plugins: Vec&amp;lt;GrpcPlugin&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Manages multiple gRPC plugin services.&lt;br/&gt;///&lt;br/&gt;/// This is safe to clone as it uses an internal reference counter.&lt;br/&gt;#[derive(Debug, Clone)]&lt;br/&gt;pub struct GrpcPluginsManager {&lt;br/&gt;    /// A collection of gRPC services, each of which contains multiple plugins.&lt;br/&gt;    pub services: Arc&amp;lt;Vec&amp;lt;GrpcPluginService&amp;gt;&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl GrpcPluginService {&lt;br/&gt;    /// Create a new service and connect to it&lt;br/&gt;    pub async fn new(service_uri: hyper::Uri) -&amp;gt; GrpcResult&amp;lt;Self&amp;gt; {&lt;br/&gt;        tracing::debug!(service_uri = %service_uri, &amp;#34;connecting to a plugins service&amp;#34;);&lt;br/&gt;        let mut client = PluginClient::connect(service_uri.clone()).await?;&lt;br/&gt;&lt;br/&gt;        tracing::debug!(service_uri = %service_uri, &amp;#34;calling `GetPlugins` RPC&amp;#34;);&lt;br/&gt;        let mut plugins = client&lt;br/&gt;            .get_plugins(Empty {})&lt;br/&gt;            .await&lt;br/&gt;            .map_err(|err| GrpcError::CallErr(err.to_string()))?&lt;br/&gt;            .into_inner()&lt;br/&gt;            .plugins;&lt;br/&gt;&lt;br/&gt;        // Sort and remove all duplicates&lt;br/&gt;        plugins.sort_unstable_by_key(|plugin| plugin.name.clone());&lt;br/&gt;        plugins.dedup_by_key(|plugin: &amp;amp;mut PluginInfo| plugin.name.clone());&lt;br/&gt;        tracing::debug!(service_uri = %service_uri, &amp;#34;received {} plugins: {plugins:?}&amp;#34;, plugins.len());&lt;br/&gt;&lt;br/&gt;        if plugins.is_empty() {&lt;br/&gt;            return Err(GrpcError::NoPlugins(service_uri));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let mut all_write_plugins = Vec::new();&lt;br/&gt;        let mut any_write_plugins = Vec::new();&lt;br/&gt;        let mut all_query_plugins = Vec::new();&lt;br/&gt;        let mut any_query_plugins = Vec::new();&lt;br/&gt;&lt;br/&gt;        for plugin in plugins.into_iter().map(GrpcPlugin::try_from) {&lt;br/&gt;            let plugin = plugin?;&lt;br/&gt;            match (plugin.is_write, plugin.is_all) {&lt;br/&gt;                (true, true) =&amp;gt; all_write_plugins.push(plugin),&lt;br/&gt;                (true, false) =&amp;gt; any_write_plugins.push(plugin),&lt;br/&gt;                (false, true) =&amp;gt; all_query_plugins.push(plugin),&lt;br/&gt;                (false, false) =&amp;gt; any_query_plugins.push(plugin),&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            uri: service_uri,&lt;br/&gt;            grpc_client: client,&lt;br/&gt;            all_write_plugins,&lt;br/&gt;            any_write_plugins,&lt;br/&gt;            all_query_plugins,&lt;br/&gt;            any_query_plugins,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl GrpcPluginsManager {&lt;br/&gt;    /// Initializes a gRPC plugins manager using the provided services.&lt;br/&gt;    ///&lt;br/&gt;    /// Connects to each service to retrieve its available plugins.&lt;br/&gt;    pub async fn with_services(services: &amp;amp;[hyper::Uri]) -&amp;gt; GrpcResult&amp;lt;Self&amp;gt; {&lt;br/&gt;        let mut manager_services = Vec::new();&lt;br/&gt;&lt;br/&gt;        for service in services {&lt;br/&gt;            manager_services.push(GrpcPluginService::new(service.clone()).await?);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            services: Arc::new(manager_services),&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the manager if gRPC plugins are configured, otherwise returns None.&lt;br/&gt;    /// Logs an error if initialization fails.&lt;br/&gt;    pub async fn maybe_manager(config: &amp;amp;RelayConfig) -&amp;gt; Option&amp;lt;Self&amp;gt; {&lt;br/&gt;        if config.plugins.grpc.is_empty() {&lt;br/&gt;            tracing::info!(&amp;#34;gRPC plugins list is empty, skipping connection&amp;#34;);&lt;br/&gt;            return None;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        match GrpcPluginsManager::with_services(&amp;amp;config.plugins.grpc).await {&lt;br/&gt;            Ok(manager) =&amp;gt; {&lt;br/&gt;                tracing::info!(&amp;#34;Successfully initialized gRPC plugins manager&amp;#34;);&lt;br/&gt;                Some(manager)&lt;br/&gt;            }&lt;br/&gt;            Err(err) =&amp;gt; {&lt;br/&gt;                tracing::error!(&amp;#34;Failed to initialize gRPC plugins. Error: {}&amp;#34;, err);&lt;br/&gt;                None&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl GrpcPlugin {&lt;br/&gt;    /// Run the plugin&lt;br/&gt;    pub async fn run(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        uri: &amp;amp;hyper::Uri,&lt;br/&gt;        client: &amp;amp;mut PluginClient,&lt;br/&gt;        body: Either&amp;lt;&amp;amp;Event, &amp;amp;Filter&amp;gt;,&lt;br/&gt;    ) -&amp;gt; Result&amp;lt;(), String&amp;gt; {&lt;br/&gt;        let request = if self.is_write {&lt;br/&gt;            assert!(body.is_left());&lt;br/&gt;            write_plugin_request(&amp;amp;self.name, body.unwrap_left())&lt;br/&gt;        } else {&lt;br/&gt;            assert!(body.is_right());&lt;br/&gt;            query_plugin_request(&amp;amp;self.name, body.unwrap_right().clone())&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        match client&lt;br/&gt;            .run_plugin(request)&lt;br/&gt;            .await&lt;br/&gt;            .map(|r| r.into_inner().plugin_response_body)&lt;br/&gt;        {&lt;br/&gt;            Ok(Some(PluginResponseBody::Accept(..))) =&amp;gt; Ok(()),&lt;br/&gt;            Ok(Some(PluginResponseBody::RejectMsg(msg))) =&amp;gt; Err(msg),&lt;br/&gt;            Ok(None) =&amp;gt; {&lt;br/&gt;                tracing::warn!(service_uri = %uri, plugin_name = %self.name, &amp;#34;plugin returns none body&amp;#34;);&lt;br/&gt;                Ok(())&lt;br/&gt;            }&lt;br/&gt;            Err(err) =&amp;gt; {&lt;br/&gt;                tracing::error!(service_uri = %uri, plugin_name = %self.name, &amp;#34;plugin returns error: {err:?}&amp;#34;);&lt;br/&gt;                Ok(())&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl GrpcPluginService {&lt;br/&gt;    /// Executes the provided `all_plugins` and `any_plugins` with the given&lt;br/&gt;    /// `body`.&lt;br/&gt;    ///&lt;br/&gt;    /// The `all_plugins` must all accept the `body` (either an `Event` or&lt;br/&gt;    /// `Filter`). If they do, the `any_plugins` are then checked. If at&lt;br/&gt;    /// least one `any_plugin` accepts the `body`, the function returns&lt;br/&gt;    /// `Ok(())`. Otherwise, it returns an `Err` with the rejection message&lt;br/&gt;    /// from the first `any_plugin` that rejected the `body`.&lt;br/&gt;    async fn run_plugins(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        body: Either&amp;lt;&amp;amp;Event, &amp;amp;Filter&amp;gt;,&lt;br/&gt;        all_plugins: &amp;amp;[GrpcPlugin],&lt;br/&gt;        any_plugins: &amp;amp;[GrpcPlugin],&lt;br/&gt;    ) -&amp;gt; Result&amp;lt;(), String&amp;gt; {&lt;br/&gt;        if all_plugins.is_empty() &amp;amp;&amp;amp; any_plugins.is_empty() {&lt;br/&gt;            return Ok(());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let mut client = self.grpc_client.clone();&lt;br/&gt;&lt;br/&gt;        // All `all_plugins` must accept the `body` to proceed.&lt;br/&gt;        for plugin in all_plugins {&lt;br/&gt;            plugin.run(&amp;amp;self.uri, &amp;amp;mut client, body).await?&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // If there are no `any_plugins`, the `body` is accepted.&lt;br/&gt;        if any_plugins.is_empty() {&lt;br/&gt;            return Ok(());&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let mut reject_msg = None;&lt;br/&gt;        for plugin in any_plugins {&lt;br/&gt;            match plugin.run(&amp;amp;self.uri, &amp;amp;mut client, body).await {&lt;br/&gt;                // If any `any_plugin` accepts the `body`, return `Ok(())`.&lt;br/&gt;                Ok(()) =&amp;gt; return Ok(()),&lt;br/&gt;                // Store the rejection message from the first `any_plugin` that rejects the `body`.&lt;br/&gt;                Err(msg) if reject_msg.is_none() =&amp;gt; reject_msg = Some(msg),&lt;br/&gt;                _ =&amp;gt; (),&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // If no `any_plugin` accepts the `body`, return the first rejection message.&lt;br/&gt;        Err(reject_msg.unwrap())&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the write plugins for the given `event`.&lt;br/&gt;    ///&lt;br/&gt;    /// Returns `Ok(())` if all `all_plugins` accept the `event` and at least&lt;br/&gt;    /// one `any_plugin` accepts it. Otherwise, returns `Err(reject_msg)`&lt;br/&gt;    /// with the rejection message.&lt;br/&gt;    pub async fn run_write_plugins(&amp;amp;self, event: &amp;amp;Event) -&amp;gt; Result&amp;lt;(), String&amp;gt; {&lt;br/&gt;        self.run_plugins(&lt;br/&gt;            Either::Left(event),&lt;br/&gt;            &amp;amp;self.all_write_plugins,&lt;br/&gt;            &amp;amp;self.any_write_plugins,&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the query plugins for the given `event`.&lt;br/&gt;    ///&lt;br/&gt;    /// Returns `Ok(())` if all `all_plugins` accept the `event` and at least&lt;br/&gt;    /// one `any_plugin` accepts it. Otherwise, returns `Err(reject_msg)`&lt;br/&gt;    /// with the rejection message.&lt;br/&gt;    pub async fn run_query_plugins(&amp;amp;self, query: &amp;amp;Filter) -&amp;gt; Result&amp;lt;(), String&amp;gt; {&lt;br/&gt;        self.run_plugins(&lt;br/&gt;            Either::Right(query),&lt;br/&gt;            &amp;amp;self.all_query_plugins,&lt;br/&gt;            &amp;amp;self.any_query_plugins,&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl TryFrom&amp;lt;PluginInfo&amp;gt; for GrpcPlugin {&lt;br/&gt;    type Error = GrpcError;&lt;br/&gt;&lt;br/&gt;    fn try_from(plugin: PluginInfo) -&amp;gt; Result&amp;lt;Self, Self::Error&amp;gt; {&lt;br/&gt;        tracing::debug!(&lt;br/&gt;            plugin_name = %plugin.name,&lt;br/&gt;            plugin_type = %plugin.plugin_type,&lt;br/&gt;            plugin_priority = %plugin.priority,&lt;br/&gt;            &amp;#34;processing plugin info&amp;#34;&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let is_write = PluginType::try_from(plugin.plugin_type)&lt;br/&gt;            .map_err(|_| GrpcError::UnknownPluginType(plugin.plugin_type))?&lt;br/&gt;            == PluginType::Write;&lt;br/&gt;&lt;br/&gt;        let is_all = PluginPriority::try_from(plugin.priority)&lt;br/&gt;            .map_err(|_| GrpcError::UnknownPluginPriority(plugin.priority))?&lt;br/&gt;            == PluginPriority::All;&lt;br/&gt;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            name: plugin.name,&lt;br/&gt;            is_write,&lt;br/&gt;            is_all,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl WritePolicy for GrpcPluginsManager {&lt;br/&gt;    fn admit_event&amp;lt;&amp;#39;a&amp;gt;(&lt;br/&gt;        &amp;amp;&amp;#39;a self,&lt;br/&gt;        event: &amp;amp;&amp;#39;a Event,&lt;br/&gt;        _: &amp;amp;&amp;#39;a std::net::SocketAddr,&lt;br/&gt;    ) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, WritePolicyResult&amp;gt; {&lt;br/&gt;        Box::pin(async {&lt;br/&gt;            for service in self.services.iter() {&lt;br/&gt;                if let Err(reject_msg) = service.run_write_plugins(event).await {&lt;br/&gt;                    tracing::debug!(service_url = %service.uri, &amp;#34;event rejected: {}&amp;#34;, event.id);&lt;br/&gt;                    return WritePolicyResult::reject(MachineReadablePrefix::Blocked, reject_msg);&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            WritePolicyResult::Accept&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl QueryPolicy for GrpcPluginsManager {&lt;br/&gt;    fn admit_query&amp;lt;&amp;#39;a&amp;gt;(&lt;br/&gt;        &amp;amp;&amp;#39;a self,&lt;br/&gt;        query: &amp;amp;&amp;#39;a Filter,&lt;br/&gt;        _: &amp;amp;&amp;#39;a std::net::SocketAddr,&lt;br/&gt;    ) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, QueryPolicyResult&amp;gt; {&lt;br/&gt;        Box::pin(async move {&lt;br/&gt;            for service in self.services.iter() {&lt;br/&gt;                if let Err(reject_msg) = service.run_query_plugins(query).await {&lt;br/&gt;                    tracing::debug!(service_url = %service.uri, &amp;#34;query rejected: {query:?}&amp;#34;);&lt;br/&gt;                    return QueryPolicyResult::reject(MachineReadablePrefix::Blocked, reject_msg);&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            QueryPolicyResult::Accept&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl PluginsManagerTrait for GrpcPluginsManager {}&lt;br/&gt;&lt;br/&gt;/// Make a new write plugin request&lt;br/&gt;fn write_plugin_request(plugin_name: &amp;amp;str, event: &amp;amp;Event) -&amp;gt; PluginRequest {&lt;br/&gt;    let tags = event&lt;br/&gt;        .tags&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|tag| {&lt;br/&gt;            plugins_api::Tag {&lt;br/&gt;                tag_kind: tag.kind().as_str().to_owned(),&lt;br/&gt;                values:   tag.as_slice()[1..].to_vec(),&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;        .collect();&lt;br/&gt;&lt;br/&gt;    PluginRequest {&lt;br/&gt;        plugin_name:         plugin_name.to_owned(),&lt;br/&gt;        plugin_request_body: Some(PluginRequestBody::Event(plugins_api::Event {&lt;br/&gt;            tags,&lt;br/&gt;            id: event.id.to_hex(),&lt;br/&gt;            public_key: event.pubkey.to_hex(),&lt;br/&gt;            created_at: event.created_at.as_secs(),&lt;br/&gt;            kind: event.kind.as_u16() as u32,&lt;br/&gt;            content: event.content.clone(),&lt;br/&gt;            signature: event.sig.to_string(),&lt;br/&gt;        })),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Make a new query plugin request&lt;br/&gt;fn query_plugin_request(plugin_name: &amp;amp;str, filter: Filter) -&amp;gt; PluginRequest {&lt;br/&gt;    let tags = filter&lt;br/&gt;        .generic_tags&lt;br/&gt;        .iter()&lt;br/&gt;        .map(|(tag, values)| {&lt;br/&gt;            plugins_api::Tag {&lt;br/&gt;                tag_kind: tag.as_str().to_owned(),&lt;br/&gt;                values:   values.clone().into_iter().collect(),&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;        .collect();&lt;br/&gt;&lt;br/&gt;    PluginRequest {&lt;br/&gt;        plugin_name:         plugin_name.to_owned(),&lt;br/&gt;        plugin_request_body: Some(PluginRequestBody::Filter(plugins_api::Filter {&lt;br/&gt;            tags,&lt;br/&gt;            ids: map_set(filter.ids, |id| id.to_hex()),&lt;br/&gt;            authors: map_set(filter.authors, |pkey| pkey.to_hex()),&lt;br/&gt;            kinds: map_set(filter.kinds, |kind| kind.as_u16() as u32),&lt;br/&gt;            since: filter.since.map(|t| t.as_secs()),&lt;br/&gt;            until: filter.until.map(|t| t.as_secs()),&lt;br/&gt;            limit: filter&lt;br/&gt;                .limit&lt;br/&gt;                .map(|limit| limit.try_into().unwrap_or(u64::MAX)),&lt;br/&gt;        })),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn map_set&amp;lt;T, R&amp;gt;(option_set: Option&amp;lt;BTreeSet&amp;lt;T&amp;gt;&amp;gt;, map_fn: impl FnMut(T) -&amp;gt; R) -&amp;gt; Vec&amp;lt;R&amp;gt; {&lt;br/&gt;    option_set&lt;br/&gt;        .map(|set| set.into_iter().map(map_fn).collect())&lt;br/&gt;        .unwrap_or_default()&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:53Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsykhy337rd3tljln4647t6jr3y6jkg56h9k0ed0le73p749v70cxszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvseq79</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsykhy337rd3tljln4647t6jr3y6jkg56h9k0ed0le73p749v70cxszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvseq79" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, EventId, Kind, Tag, TagKind, TagStandard},&lt;br/&gt;    filter::{Alphabet, Filter, SingleLetterTag},&lt;br/&gt;    message::MachineReadablePrefix,&lt;br/&gt;    nips::{nip01::Coordinate, nip19::ToBech32},&lt;br/&gt;    util::BoxedFuture,&lt;br/&gt;};&lt;br/&gt;use nostr_database::NostrDatabase;&lt;br/&gt;use nostr_relay_builder::builder::WritePolicyResult;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    ext_traits::WritePolicyResultExt,&lt;br/&gt;    git_server::utils as git_utils,&lt;br/&gt;    relay::plugins_manager::RelayPlugin,&lt;br/&gt;    utils,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Determines if a repository announcement event is valid.&lt;br/&gt;pub struct ValidateRepoEvent;&lt;br/&gt;&lt;br/&gt;/// Checks if a repository is a GRASP repository and rejects it if not.&lt;br/&gt;pub struct GraspRepo {&lt;br/&gt;    domain: String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Accept events that tag or tagged by accepted git repository announcement or&lt;br/&gt;/// accepted issues or patches&lt;br/&gt;pub struct AcceptMention {&lt;br/&gt;    pub db: Arc&amp;lt;dyn NostrDatabase&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Rejects the repository state announcement if we don&amp;#39;t have the repository&lt;br/&gt;pub struct RejectRepoState {&lt;br/&gt;    pub db: Arc&amp;lt;dyn NostrDatabase&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Checks if the repository state announcement is valid.&lt;br/&gt;/// This ensures the `d` tag is correct, `HEAD` is present, and the `HEAD` ref&lt;br/&gt;/// exists.&lt;br/&gt;pub struct ValidateRepoState;&lt;br/&gt;&lt;br/&gt;impl GraspRepo {&lt;br/&gt;    /// Constructs a new [GraspRepo] plugin&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn new(relay_domain: impl Into&amp;lt;String&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        GraspRepo {&lt;br/&gt;            domain: relay_domain.into(),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl AcceptMention {&lt;br/&gt;    #[inline]&lt;br/&gt;    /// Constructs a new [AcceptMention] plugin&lt;br/&gt;    pub fn new(db: Arc&amp;lt;dyn NostrDatabase&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        Self { db }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if any event matching the given filter in the database.&lt;br/&gt;    async fn db_contains(&amp;amp;self, filter: Filter) -&amp;gt; bool {&lt;br/&gt;        match self.db.count(filter.clone()).await {&lt;br/&gt;            Ok(count) =&amp;gt; count != 0,&lt;br/&gt;            Err(err) =&amp;gt; {&lt;br/&gt;                tracing::error!(error = %err, filter = ?filter, &amp;#34;Failed to query the database&amp;#34;);&lt;br/&gt;                false&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RejectRepoState {&lt;br/&gt;    #[inline]&lt;br/&gt;    /// Constructs a new [RejectRepoState] plugin&lt;br/&gt;    pub fn new(db: Arc&amp;lt;dyn NostrDatabase&amp;gt;) -&amp;gt; Self {&lt;br/&gt;        Self { db }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RelayPlugin for ValidateRepoEvent {&lt;br/&gt;    fn check_event&amp;lt;&amp;#39;a&amp;gt;(&amp;amp;&amp;#39;a self, event: &amp;amp;&amp;#39;a Event) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, Option&amp;lt;WritePolicyResult&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async {&lt;br/&gt;            if event.kind != Kind::GitRepoAnnouncement {&lt;br/&gt;                return None;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // At least one relay&lt;br/&gt;            if event&lt;br/&gt;                .tags&lt;br/&gt;                .find(TagKind::Relays)&lt;br/&gt;                .is_none_or(|relays| relays.content().is_none())&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;No relays in the repository announcement&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // At least one clone url&lt;br/&gt;            if event&lt;br/&gt;                .tags&lt;br/&gt;                .find(TagKind::Clone)&lt;br/&gt;                .is_none_or(|clones| clones.content().is_none())&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;No clone urls in the repository announcement&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if event.tags.filter(TagKind::d()).count() &amp;gt; 1 {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;More than one `d` tag in the repository announcement&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let Some(repo_name) = event.tags.identifier() else {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;The repository announcement must contains `d` tag&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            };&lt;br/&gt;&lt;br/&gt;            if repo_name.chars().count() &amp;gt; 30 {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;Repository name exceeds maximum length of 30 characters&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // Check if it&amp;#39;s a valid name. All ascii and no whitespace&lt;br/&gt;            if repo_name&lt;br/&gt;                .chars()&lt;br/&gt;                .any(|c| !c.is_ascii_alphanumeric() &amp;amp;&amp;amp; c != &amp;#39;-&amp;#39; &amp;amp;&amp;amp; c != &amp;#39;_&amp;#39;)&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(format!(&lt;br/&gt;                    &amp;#34;Invalid repository name &amp;#39;{repo_name}&amp;#39;. Repository names can only contain \&lt;br/&gt;                     ASCII letters, numbers, hyphens (-), and underscores (_).&amp;#34;&lt;br/&gt;                )));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            Some(WritePolicyResult::Accept)&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RelayPlugin for GraspRepo {&lt;br/&gt;    fn check_event&amp;lt;&amp;#39;a&amp;gt;(&amp;amp;&amp;#39;a self, event: &amp;amp;&amp;#39;a Event) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, Option&amp;lt;WritePolicyResult&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async {&lt;br/&gt;            if event.kind != Kind::GitRepoAnnouncement {&lt;br/&gt;                return None;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let repo_name = event&lt;br/&gt;                .tags&lt;br/&gt;                .identifier()&lt;br/&gt;                .expect(&amp;#34;Verified in ValidateRepoEvent&amp;#34;);&lt;br/&gt;&lt;br/&gt;            let relays = event&lt;br/&gt;                .tags&lt;br/&gt;                .find(TagKind::Relays)&lt;br/&gt;                .map(|t| &amp;amp;t.as_slice()[1..])&lt;br/&gt;                .expect(&amp;#34;Verified in ValidateRepoEvent&amp;#34;);&lt;br/&gt;&lt;br/&gt;            let clones = event&lt;br/&gt;                .tags&lt;br/&gt;                .find(TagKind::Clone)&lt;br/&gt;                .map(|t| &amp;amp;t.as_slice()[1..])&lt;br/&gt;                .expect(&amp;#34;Verified in ValidateRepoEvent&amp;#34;);&lt;br/&gt;&lt;br/&gt;            let repo_clone_url = format!(&lt;br/&gt;                &amp;#34;{}/{}/{repo_name}.git&amp;#34;,&lt;br/&gt;                self.domain,&lt;br/&gt;                event.pubkey.to_bech32().expect(&amp;#34;Infallible&amp;#34;)&lt;br/&gt;            );&lt;br/&gt;&lt;br/&gt;            if !relays&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|relay| utils::remove_proto(relay).starts_with(&amp;amp;self.domain))&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(format!(&lt;br/&gt;                    &amp;#34;`{}` relay is not listed in the &amp;#39;relays&amp;#39; tag of the announcement&amp;#34;,&lt;br/&gt;                    self.domain&lt;br/&gt;                )));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if !clones&lt;br/&gt;                .iter()&lt;br/&gt;                .any(|clone_url| utils::remove_proto(clone_url) == repo_clone_url)&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(format!(&lt;br/&gt;                    &amp;#34;`{}` relay does not match any URLs in the &amp;#39;clone&amp;#39; tag of the announcement&amp;#34;,&lt;br/&gt;                    self.domain&lt;br/&gt;                )));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            Some(WritePolicyResult::Accept)&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RelayPlugin for AcceptMention {&lt;br/&gt;    fn check_event&amp;lt;&amp;#39;a&amp;gt;(&amp;amp;&amp;#39;a self, event: &amp;amp;&amp;#39;a Event) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, Option&amp;lt;WritePolicyResult&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async {&lt;br/&gt;            // from GRASP protocol:&lt;br/&gt;            // &amp;#34;MUST accept other events that tag, or are tagged by, either:&lt;br/&gt;            // 1. accepted git repository announcements; or&lt;br/&gt;            // 2. accepted issues or patches&amp;#34;&lt;br/&gt;&lt;br/&gt;            // Check if the event tag a repository announcement&lt;br/&gt;            if self&lt;br/&gt;                .db_contains(Filter::new().coordinates(repos_coordinate(event)))&lt;br/&gt;                .await&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::Accept);&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // Check if the event tag a patch or an issue&lt;br/&gt;            if self&lt;br/&gt;                .db_contains(&lt;br/&gt;                    Filter::new()&lt;br/&gt;                        .kinds([Kind::GitPatch, Kind::GitIssue])&lt;br/&gt;                        .ids(tagged_events(event)),&lt;br/&gt;                )&lt;br/&gt;                .await&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::Accept);&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            // Check if the event is tagged by a patch or an issue. By either `e` tag or `q`&lt;br/&gt;            // tag&lt;br/&gt;            if self&lt;br/&gt;                .db_contains(&lt;br/&gt;                    Filter::new()&lt;br/&gt;                        .kinds([Kind::GitPatch, Kind::GitIssue])&lt;br/&gt;                        .event(event.id),&lt;br/&gt;                )&lt;br/&gt;                .await&lt;br/&gt;                || self&lt;br/&gt;                    .db_contains(&lt;br/&gt;                        Filter::new()&lt;br/&gt;                            .kinds([Kind::GitPatch, Kind::GitIssue])&lt;br/&gt;                            .custom_tag(SingleLetterTag::lowercase(Alphabet::Q), event.id),&lt;br/&gt;                    )&lt;br/&gt;                    .await&lt;br/&gt;            {&lt;br/&gt;                return Some(WritePolicyResult::Accept);&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            None&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RelayPlugin for ValidateRepoState {&lt;br/&gt;    fn check_event&amp;lt;&amp;#39;a&amp;gt;(&amp;amp;&amp;#39;a self, event: &amp;amp;&amp;#39;a Event) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, Option&amp;lt;WritePolicyResult&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async {&lt;br/&gt;            if event.kind != Kind::RepoState {&lt;br/&gt;                return None;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if event.tags.filter(TagKind::d()).count() &amp;gt; 1 {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;Invalid repository state announcement. More than one `d` tag&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let Some(repo_name) = event.tags.identifier() else {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;Invalid repository state announcement. No `d` tag&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            };&lt;br/&gt;&lt;br/&gt;            if repo_name.chars().count() &amp;gt; 30 {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;Repository name exceeds maximum length of 30 characters&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let Some(mut head) = event.tags.find(TagKind::Head).and_then(Tag::content) else {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;No `HEAD` tag in the repository state announcement&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            };&lt;br/&gt;&lt;br/&gt;            if !head.starts_with(&amp;#34;ref: refs/heads/&amp;#34;) {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;The `HEAD` tag must start with `ref: refs/heads/`&amp;#34;,&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            for (ref_name, commit) in git_utils::extract_refs(event) {&lt;br/&gt;                if !utils::is_valid_sha1(commit) {&lt;br/&gt;                    return Some(WritePolicyResult::blocked_reject(format!(&lt;br/&gt;                        &amp;#34;`{ref_name}` has an invalid sha1 commit id&amp;#34;&lt;br/&gt;                    )));&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            head = head.trim_start_matches(&amp;#34;ref: &amp;#34;).trim();&lt;br/&gt;            if !git_utils::extract_refs(event).any(|(ref_name, _)| ref_name == head) {&lt;br/&gt;                return Some(WritePolicyResult::blocked_reject(format!(&lt;br/&gt;                    &amp;#34;No ref for the head `{head}`&amp;#34;&lt;br/&gt;                )));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            Some(WritePolicyResult::Accept)&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl RelayPlugin for RejectRepoState {&lt;br/&gt;    fn check_event&amp;lt;&amp;#39;a&amp;gt;(&amp;amp;&amp;#39;a self, event: &amp;amp;&amp;#39;a Event) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, Option&amp;lt;WritePolicyResult&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async {&lt;br/&gt;            if event.kind != Kind::RepoState {&lt;br/&gt;                return None;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            let repo_name = event&lt;br/&gt;                .tags&lt;br/&gt;                .identifier()&lt;br/&gt;                .expect(&amp;#34;Verified in ValidateRepoState&amp;#34;);&lt;br/&gt;&lt;br/&gt;            // Get the repositories with the same identifier&lt;br/&gt;            let events = match self&lt;br/&gt;                .db&lt;br/&gt;                .query(&lt;br/&gt;                    Filter::new()&lt;br/&gt;                        .kind(Kind::GitRepoAnnouncement)&lt;br/&gt;                        .identifier(repo_name)&lt;br/&gt;                        .limit(100),&lt;br/&gt;                )&lt;br/&gt;                .await&lt;br/&gt;            {&lt;br/&gt;                Ok(events) =&amp;gt; events,&lt;br/&gt;                Err(err) =&amp;gt; {&lt;br/&gt;                    tracing::error!(&amp;#34;Database error: {err}&amp;#34;);&lt;br/&gt;                    return Some(WritePolicyResult::reject(&lt;br/&gt;                        MachineReadablePrefix::Error,&lt;br/&gt;                        &amp;#34;Database error&amp;#34;,&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;            };&lt;br/&gt;&lt;br/&gt;            // Accept the state announcement if the repository state author is an author or&lt;br/&gt;            // a maintainer in any of the repositories&lt;br/&gt;            if events.iter().any(|repo_announcement| {&lt;br/&gt;                repo_announcement.pubkey == event.pubkey&lt;br/&gt;                    || utils::get_maintainers(repo_announcement)&lt;br/&gt;                        .any(|maintainer| maintainer == &amp;amp;event.pubkey)&lt;br/&gt;            }) {&lt;br/&gt;                Some(WritePolicyResult::Accept)&lt;br/&gt;            } else {&lt;br/&gt;                Some(WritePolicyResult::blocked_reject(&lt;br/&gt;                    &amp;#34;You don&amp;#39;t have a repository for this state announcement in the relay.&amp;#34;,&lt;br/&gt;                ))&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extracts up to 20 repository coordinates from an event&amp;#39;s tags.&lt;br/&gt;/// Only includes coordinates marked as Git repository announcements.&lt;br/&gt;fn repos_coordinate(event: &amp;amp;Event) -&amp;gt; impl Iterator&amp;lt;Item = &amp;amp;Coordinate&amp;gt; {&lt;br/&gt;    event&lt;br/&gt;        .tags&lt;br/&gt;        .as_slice()&lt;br/&gt;        .iter()&lt;br/&gt;        .filter_map(|t| {&lt;br/&gt;            if let TagStandard::Coordinate { coordinate, .. } = t.as_standardized()?&lt;br/&gt;                &amp;amp;&amp;amp; coordinate.kind == Kind::GitRepoAnnouncement&lt;br/&gt;            {&lt;br/&gt;                return Some(coordinate);&lt;br/&gt;            }&lt;br/&gt;            None&lt;br/&gt;        })&lt;br/&gt;        .take(20)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extract up to 20 tagged events from an event&amp;#39;s tags.&lt;br/&gt;fn tagged_events(event: &amp;amp;Event) -&amp;gt; impl Iterator&amp;lt;Item = EventId&amp;gt; {&lt;br/&gt;    event&lt;br/&gt;        .tags&lt;br/&gt;        .as_slice()&lt;br/&gt;        .iter()&lt;br/&gt;        .filter_map(|t| {&lt;br/&gt;            if let TagStandard::Event { event_id, .. } = t.as_standardized()? {&lt;br/&gt;                return Some(*event_id);&lt;br/&gt;            }&lt;br/&gt;            None&lt;br/&gt;        })&lt;br/&gt;        .take(20)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:39Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsydzgz3klermh6luh5745kuz9muunls02tcgf4224lvudsf3hwfyqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvswmytq</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsydzgz3klermh6luh5745kuz9muunls02tcgf4224lvudsf3hwfyqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvswmytq" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    body::Bytes,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;&lt;br/&gt;use super::git_command::GitCommand;&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;/// Handles a git-upload-pack request for a repository.&lt;br/&gt;/// Verifies the repository exists and processes the received pack data.&lt;br/&gt;/// Returns a successful response with the pack data or an appropriate error.&lt;br/&gt;pub async fn upload_pack(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    headers: hyper::HeaderMap,&lt;br/&gt;    body: Bytes,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    match GitCommand::new(&amp;amp;state.config.grasp.git_path, &amp;amp;repo_path)&lt;br/&gt;        .upload_pack(&amp;amp;body, super::utils::contains_git_v2(&amp;amp;headers))&lt;br/&gt;        .await&lt;br/&gt;    {&lt;br/&gt;        Ok(response_body) =&amp;gt; {&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::OK)&lt;br/&gt;                .header(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/x-git-upload-pack-result&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Connection&amp;#34;, &amp;#34;Keep-Alive&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Transfer-Encoding&amp;#34;, &amp;#34;chunked&amp;#34;)&lt;br/&gt;                .header(&amp;#34;X-Content-Type-Options&amp;#34;, &amp;#34;nosniff&amp;#34;)&lt;br/&gt;                .body(response_body)&lt;br/&gt;                .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;        Err(err_msg) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, err_msg).into_response(),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:39Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsds9rqwfsjzzjzjxsqu6ewflt6dmp22pm9z47yeys900cela0vkugzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsveuw0nu</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsds9rqwfsjzzjzjxsqu6ewflt6dmp22pm9z47yeys900cela0vkugzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsveuw0nu" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use crate::utils;&lt;br/&gt;&lt;br/&gt;/// The null/zero SHA-1 hash indicating deletion or non-existence.&lt;br/&gt;const NULL_OID: &amp;amp;str = &amp;#34;0000000000000000000000000000000000000000&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Represents a ref update pkt-line in a Git protocol.&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// The old commit hash (before the update).&lt;br/&gt;    pub old_commit:   &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// The new commit hash (after the update).&lt;br/&gt;    pub new_commit:   &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// The name of the reference (e.g., &amp;#34;refs/heads/master&amp;#34;).&lt;br/&gt;    pub ref_name:     &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// The capabilities.&lt;br/&gt;    pub capabilities: Option&amp;lt;&amp;amp;&amp;#39;a str&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;a&amp;gt; RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// Parses the ref update payload (without the 4-byte length prefix).&lt;br/&gt;    ///&lt;br/&gt;    /// Expected format: `&amp;lt;old-oid&amp;gt; &amp;lt;new-oid&amp;gt; &amp;lt;ref-name&amp;gt;[\0&amp;lt;capabilities&amp;gt;]`&lt;br/&gt;    pub fn parse(payload: &amp;amp;&amp;#39;a [u8]) -&amp;gt; Result&amp;lt;Self, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let payload = payload.strip_suffix(b&amp;#34;\n&amp;#34;).unwrap_or(payload);&lt;br/&gt;&lt;br/&gt;        let str_payload =&lt;br/&gt;            str::from_utf8(payload).map_err(|_| &amp;#34;Invalid pkt-line content: not valid UTF-8&amp;#34;)?;&lt;br/&gt;        let mut parts = str_payload.splitn(3, &amp;#39; &amp;#39;);&lt;br/&gt;&lt;br/&gt;        let old_commit = parts.next().ok_or(&amp;#34;Missing old commit hash&amp;#34;)?;&lt;br/&gt;        let new_commit = parts.next().ok_or(&amp;#34;Missing new commit hash&amp;#34;)?;&lt;br/&gt;        let ref_with_caps = parts.next().ok_or(&amp;#34;Missing ref name&amp;#34;)?;&lt;br/&gt;&lt;br/&gt;        // pack-protocol: PKT-LINE(command NUL capability-list)&lt;br/&gt;        let (ref_name, capabilities) = match ref_with_caps.split_once(&amp;#39;\0&amp;#39;) {&lt;br/&gt;            Some((name, caps)) =&amp;gt; (name, Some(caps.trim())),&lt;br/&gt;            None =&amp;gt; (ref_with_caps, None),&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        // Validate OIDs&lt;br/&gt;        if !utils::is_valid_sha1(old_commit) {&lt;br/&gt;            return Err(&amp;#34;Invalid old commit hash&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        if !utils::is_valid_sha1(new_commit) {&lt;br/&gt;            return Err(&amp;#34;Invalid new commit hash&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // Validate ref name&lt;br/&gt;        if !ref_name.starts_with(&amp;#34;refs/&amp;#34;) {&lt;br/&gt;            return Err(&amp;#34;Invalid ref name: must start with &amp;#39;refs/&amp;#39;&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            old_commit,&lt;br/&gt;            new_commit,&lt;br/&gt;            ref_name,&lt;br/&gt;            capabilities,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `true` if this is a branch/ref deletion (new commit is null&lt;br/&gt;    /// OID).&lt;br/&gt;    #[inline]&lt;br/&gt;    #[must_use]&lt;br/&gt;    pub fn is_delete(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.new_commit == NULL_OID&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `true` if this is a new branch/ref creation (old commit is null&lt;br/&gt;    /// OID).&lt;br/&gt;    #[allow(dead_code)] // Used in unit tests&lt;br/&gt;    pub fn is_create(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.old_commit == NULL_OID&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses the 4-byte hex length prefix of a pkt-line.&lt;br/&gt;/// Returns `None` for special packets (flush=0000, delim=0001, etc.)&lt;br/&gt;fn parse_pkt_length(data: &amp;amp;[u8]) -&amp;gt; Result&amp;lt;Option&amp;lt;usize&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    if data.len() &amp;lt; 4 {&lt;br/&gt;        return Err(&amp;#34;Invalid pkt-line: input too short&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let len_str =&lt;br/&gt;        str::from_utf8(&amp;amp;data[..4]).map_err(|_| &amp;#34;Invalid pkt-line length: not valid UTF-8&amp;#34;)?;&lt;br/&gt;&lt;br/&gt;    let len = u16::from_str_radix(len_str, 16)&lt;br/&gt;        .map_err(|_| &amp;#34;Invalid pkt-line length: not valid hex&amp;#34;)? as usize;&lt;br/&gt;&lt;br/&gt;    match len {&lt;br/&gt;        0 =&amp;gt; Ok(None), // flush packet (0000)&lt;br/&gt;        1 =&amp;gt; Ok(None), // delimiter packet (0001) - skip&lt;br/&gt;        2 =&amp;gt; Ok(None), // response-end packet (0002) - skip&lt;br/&gt;        3 =&amp;gt; Err(&amp;#34;Invalid pkt-line: length too short&amp;#34;),&lt;br/&gt;        _ =&amp;gt; Ok(Some(len)),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn collect_pkt_lines(body: &amp;amp;[u8]) -&amp;gt; Result&amp;lt;Vec&amp;lt;&amp;amp;[u8]&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    let mut pkts = Vec::new();&lt;br/&gt;    let mut pos = 0;&lt;br/&gt;&lt;br/&gt;    while pos &amp;lt; body.len() {&lt;br/&gt;        // Need at least 4 bytes for the length prefix&lt;br/&gt;        if body.len() - pos &amp;lt; 4 {&lt;br/&gt;            break;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // Parse length prefix&lt;br/&gt;        let Some(pkt_len) = parse_pkt_length(&amp;amp;body[pos..])? else {&lt;br/&gt;            break;&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        // Validate we have enough data&lt;br/&gt;        if pos &#43; pkt_len &amp;gt; body.len() {&lt;br/&gt;            return Err(&amp;#34;Invalid pkt-line: declared length exceeds data length&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // push the pkt (skip the 4-byte length prefix)&lt;br/&gt;        let pkt = &amp;amp;body[pos &#43; 4..pos &#43; pkt_len];&lt;br/&gt;        pkts.push(pkt);&lt;br/&gt;&lt;br/&gt;        pos &#43;= pkt_len;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(pkts)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses pkt-line formatted ref-updates from a byte slice.&lt;br/&gt;///&lt;br/&gt;/// Stops parsing when encountering a flush packet (0000) or end of data.&lt;br/&gt;/// Returns the parsed ref updates&lt;br/&gt;pub fn parse_pkt_lines&amp;lt;&amp;#39;a&amp;gt;(body: &amp;amp;&amp;#39;a [u8]) -&amp;gt; Result&amp;lt;Vec&amp;lt;RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt;&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    let mut ref_updates = Vec::new();&lt;br/&gt;&lt;br/&gt;    // Parse only pkts that looks like command, where the command is:&lt;br/&gt;    //  command =  create / delete / update&lt;br/&gt;    //  create  =  zero-id SP new-id  SP name&lt;br/&gt;    //  delete  =  old-id  SP zero-id SP name&lt;br/&gt;    //  update  =  old-id  SP new-id  SP name&lt;br/&gt;    //&lt;br/&gt;    //  old-id  =  obj-id&lt;br/&gt;    //  new-id  =  obj-id&lt;br/&gt;    //&lt;br/&gt;    // This because we may receive commands and may receive `push-cert` that&lt;br/&gt;    // contains commands `*PKT-LINE(command LF)`. So this parsers will works with&lt;br/&gt;    // both.&lt;br/&gt;    for pkt in collect_pkt_lines(body)? {&lt;br/&gt;        let Ok(pkt_str) = str::from_utf8(pkt) else {&lt;br/&gt;            continue;&lt;br/&gt;        };&lt;br/&gt;        let mut parts = pkt_str.split(&amp;#39; &amp;#39;);&lt;br/&gt;&lt;br/&gt;        if utils::is_valid_sha1(parts.next().unwrap_or_default())&lt;br/&gt;            &amp;amp;&amp;amp; utils::is_valid_sha1(parts.next().unwrap_or_default())&lt;br/&gt;        {&lt;br/&gt;            // it looks like a command&lt;br/&gt;            ref_updates.push(RefUpdatePkt::parse(pkt)?);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(ref_updates)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests {&lt;br/&gt;    use super::*;&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn parse_single_ref_update() {&lt;br/&gt;        let data = b&amp;#34;006bac281124fd463f368106445a4fe4eb251d9c7d7a 4559b8048c334a7e61c76a622cf7cd578a6af406 refs/heads/test2-file&amp;#34;;&lt;br/&gt;&lt;br/&gt;        let updates = parse_pkt_lines(data).unwrap();&lt;br/&gt;        assert_eq!(updates.len(), 1);&lt;br/&gt;        assert_eq!(updates[0].ref_name, &amp;#34;refs/heads/test2-file&amp;#34;);&lt;br/&gt;        assert_eq!(&lt;br/&gt;            updates[0].old_commit,&lt;br/&gt;            &amp;#34;ac281124fd463f368106445a4fe4eb251d9c7d7a&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(&lt;br/&gt;            updates[0].new_commit,&lt;br/&gt;            &amp;#34;4559b8048c334a7e61c76a622cf7cd578a6af406&amp;#34;&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn parse_create_branch() {&lt;br/&gt;        let payload = b&amp;#34;0000000000000000000000000000000000000000 53e284c5c3e8b8310077a43d09fd391456f582df refs/heads/new-branch&amp;#34;;&lt;br/&gt;        let update = RefUpdatePkt::parse(payload).unwrap();&lt;br/&gt;&lt;br/&gt;        assert!(update.is_create());&lt;br/&gt;        assert!(!update.is_delete());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn parse_delete_branch() {&lt;br/&gt;        let payload = b&amp;#34;53e284c5c3e8b8310077a43d09fd391456f582df 0000000000000000000000000000000000000000 refs/heads/old-branch&amp;#34;;&lt;br/&gt;        let update = RefUpdatePkt::parse(payload).unwrap();&lt;br/&gt;&lt;br/&gt;        assert!(update.is_delete());&lt;br/&gt;        assert!(!update.is_create());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn flush_stops_parsing() {&lt;br/&gt;        let mut data = Vec::new();&lt;br/&gt;        data.extend_from_slice(b&amp;#34;00b10000000000000000000000000000000000000000 ac281124fd463f368106445a4fe4eb251d9c7d7a refs/heads/master\0report-status-v2 side-band-64k object-format=sha1 agent=git/2.51.2-Linux\n&amp;#34;);&lt;br/&gt;        data.extend_from_slice(b&amp;#34;0000&amp;#34;); // Flush&lt;br/&gt;        data.extend_from_slice(b&amp;#34;PACK\x00\x00\x00\x02&amp;#34;); // PACK header&lt;br/&gt;&lt;br/&gt;        let updates = parse_pkt_lines(&amp;amp;data).unwrap();&lt;br/&gt;        assert_eq!(updates.len(), 1);&lt;br/&gt;&lt;br/&gt;        let update = updates.first().unwrap();&lt;br/&gt;        assert!(update.is_create());&lt;br/&gt;        assert_eq!(&lt;br/&gt;            update.new_commit,&lt;br/&gt;            &amp;#34;ac281124fd463f368106445a4fe4eb251d9c7d7a&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(update.ref_name, &amp;#34;refs/heads/master&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn two_ref_updates() {&lt;br/&gt;        let mut data = Vec::new();&lt;br/&gt;        // first pkt-line&lt;br/&gt;        data.extend_from_slice(b&amp;#34;00B2ac281124fd463f368106445a4fe4eb251d9c7d7a 4559b8048c334a7e61c76a622cf7cd578a6af406 refs/heads/master\0 report-status-v2 side-band-64k object-format=sha1 agent=git/2.51.2-Linux\n&amp;#34;);&lt;br/&gt;        // second pkt-line&lt;br/&gt;        data.extend_from_slice(b&amp;#34;006b4559b8048c334a7e61c76a622cf7cd578a6af406 53e284c5c3e8b8310077a43d09fd391456f582df refs/heads/test2-file&amp;#34;);&lt;br/&gt;        data.extend_from_slice(b&amp;#34;0000&amp;#34;); // Flush&lt;br/&gt;        data.extend_from_slice(b&amp;#34;PACK\x00\x00\x00\x02&amp;#34;); // PACK header&lt;br/&gt;&lt;br/&gt;        let updates = parse_pkt_lines(&amp;amp;data).unwrap();&lt;br/&gt;        assert_eq!(updates.len(), 2);&lt;br/&gt;        let first_update = updates.first().unwrap();&lt;br/&gt;        let second_update = updates.last().unwrap();&lt;br/&gt;&lt;br/&gt;        assert_eq!(&lt;br/&gt;            first_update.old_commit,&lt;br/&gt;            &amp;#34;ac281124fd463f368106445a4fe4eb251d9c7d7a&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(&lt;br/&gt;            first_update.new_commit,&lt;br/&gt;            &amp;#34;4559b8048c334a7e61c76a622cf7cd578a6af406&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(first_update.ref_name, &amp;#34;refs/heads/master&amp;#34;);&lt;br/&gt;&lt;br/&gt;        assert_eq!(&lt;br/&gt;            second_update.old_commit,&lt;br/&gt;            &amp;#34;4559b8048c334a7e61c76a622cf7cd578a6af406&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(&lt;br/&gt;            second_update.new_commit,&lt;br/&gt;            &amp;#34;53e284c5c3e8b8310077a43d09fd391456f582df&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(second_update.ref_name, &amp;#34;refs/heads/test2-file&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:28Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsvqfjjn57ngze0z0kg46tvj695dkyxeq0e6srleev8tpjdr909ccqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgrz0my</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsvqfjjn57ngze0z0kg46tvj695dkyxeq0e6srleev8tpjdr909ccqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgrz0my" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// GRASP Git command handler&lt;br/&gt;pub mod git_command;&lt;br/&gt;/// GRASP middlewares&lt;br/&gt;pub mod middlewares;&lt;br/&gt;/// GRASP plugins&lt;br/&gt;pub mod plugins;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:28Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsg6t77w2cm7p7smj5tz85c64ydeta66k3zj566pezda9pgml0u35qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv796m76</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsg6t77w2cm7p7smj5tz85c64ydeta66k3zj566pezda9pgml0u35qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv796m76" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    body::Bytes,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;use nostr::nips::nip19::ToBech32;&lt;br/&gt;&lt;br/&gt;use super::{git_command::GitCommand, ref_update_pkt_line::parse_pkt_lines, utils};&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;/// Handles a git-receive-pack request for a repository.&lt;br/&gt;/// Verifies the repository exists and processes the received pack data.&lt;br/&gt;/// Returns a successful response with the pack data or an appropriate error.&lt;br/&gt;pub async fn receive_pack(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    body: Bytes,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let ref_updates = match parse_pkt_lines(&amp;amp;body) {&lt;br/&gt;        Ok(ref_updates) =&amp;gt; ref_updates,&lt;br/&gt;        Err(err) =&amp;gt; return (StatusCode::BAD_REQUEST, err).into_response(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&lt;br/&gt;        &amp;#34;Ref updates: {ref_updates:?} of repo `{}/{}.git`&amp;#34;,&lt;br/&gt;        params.public_key.to_bech32().expect(&amp;#34;Infallible&amp;#34;),&lt;br/&gt;        params.repo_name&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    let capabilities = ref_updates&lt;br/&gt;        .iter()&lt;br/&gt;        .find_map(|ref_update| ref_update.capabilities)&lt;br/&gt;        .unwrap_or_default();&lt;br/&gt;&lt;br/&gt;    let refs_errors = match utils::is_legal_push(&amp;amp;ref_updates, &amp;amp;state.database, &amp;amp;params).await {&lt;br/&gt;        Ok(refs) =&amp;gt; refs,&lt;br/&gt;        Err((status_code, err)) =&amp;gt; {&lt;br/&gt;            tracing::error!(err = %err, &amp;#34;Failed to check the ref updates&amp;#34;);&lt;br/&gt;            return (status_code, err).into_response();&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    if !refs_errors.is_empty() {&lt;br/&gt;        return utils::git_receive_pack_error(&amp;amp;refs_errors, &amp;amp;ref_updates, capabilities);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    match GitCommand::new(&amp;amp;state.config.grasp.git_path, &amp;amp;repo_path)&lt;br/&gt;        .receive_pack(&amp;amp;body)&lt;br/&gt;        .await&lt;br/&gt;    {&lt;br/&gt;        Ok(response_body) =&amp;gt; {&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::OK)&lt;br/&gt;                .header(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/x-git-receive-pack-result&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Connection&amp;#34;, &amp;#34;Keep-Alive&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Transfer-Encoding&amp;#34;, &amp;#34;chunked&amp;#34;)&lt;br/&gt;                .header(&amp;#34;X-Content-Type-Options&amp;#34;, &amp;#34;nosniff&amp;#34;)&lt;br/&gt;                .body(response_body)&lt;br/&gt;                .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;        Err(err_msg) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, err_msg).into_response(),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:17Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqszw95lhyv84gf0nz24qxdgrrqak2krvnhjusj6s7rl8skgy3uc2eszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvs8gkec</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqszw95lhyv84gf0nz24qxdgrrqak2krvnhjusj6s7rl8skgy3uc2eszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvs8gkec" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use either::Either;&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, Kind, TagKind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;    nips::nip19::ToBech32,&lt;br/&gt;    util::BoxedFuture,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::relay::{MiddlewareState, grasp::git_command::GraspGitCommand};&lt;br/&gt;&lt;br/&gt;/// Creates a repo if received a repository announcements contains the relay&lt;br/&gt;/// domain in `clone` tag and `relay` tag&lt;br/&gt;pub fn repo_creator&amp;lt;&amp;#39;a&amp;gt;(&lt;br/&gt;    repo_or_query: Either&amp;lt;&amp;amp;&amp;#39;a Filter, &amp;amp;&amp;#39;a Event&amp;gt;,&lt;br/&gt;    state: Arc&amp;lt;MiddlewareState&amp;gt;,&lt;br/&gt;) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, ()&amp;gt; {&lt;br/&gt;    Box::pin(async move {&lt;br/&gt;        let Either::Right(event) = repo_or_query else {&lt;br/&gt;            return;&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        if event.kind != Kind::GitRepoAnnouncement {&lt;br/&gt;            return;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let Some(repo_name) = event.tags.identifier() else {&lt;br/&gt;            return;&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let repo_description = event&lt;br/&gt;            .tags&lt;br/&gt;            .find(TagKind::Description)&lt;br/&gt;            .and_then(|t| t.content())&lt;br/&gt;            .unwrap_or(&amp;#34;N/A&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if let Err(err) = GraspGitCommand::new(&lt;br/&gt;            &amp;amp;state.config.grasp.git_path,&lt;br/&gt;            &amp;amp;state&lt;br/&gt;                .config&lt;br/&gt;                .grasp&lt;br/&gt;                .repos_path&lt;br/&gt;                .join(event.pubkey.to_bech32().expect(&amp;#34;Infallible&amp;#34;))&lt;br/&gt;                .join(repo_name)&lt;br/&gt;                .with_extension(&amp;#34;git&amp;#34;),&lt;br/&gt;        )&lt;br/&gt;        .new_bare(repo_description)&lt;br/&gt;        .await&lt;br/&gt;        {&lt;br/&gt;            tracing::error!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;    })&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:17Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsgml2uqf0p4ttat3d8xga3x7fcagd4s4supy9y4re2u2pzsck54mczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvs4vu79</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsgml2uqf0p4ttat3d8xga3x7fcagd4s4supy9y4re2u2pzsck54mczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvs4vu79" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::{Component, PathBuf};&lt;br/&gt;&lt;br/&gt;use axum::{extract::FromRequestParts, http::request::Parts};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;use nostr::key::PublicKey;&lt;br/&gt;&lt;br/&gt;/// Parameters containing a repository author&amp;#39;s public key and repository name.&lt;br/&gt;#[derive(serde::Deserialize)]&lt;br/&gt;pub struct PublicKeyAndRepoPath {&lt;br/&gt;    /// The author&amp;#39;s Nostr public key.&lt;br/&gt;    pub public_key: PublicKey,&lt;br/&gt;    /// The full repository name.&lt;br/&gt;    pub repo_name:  String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Either `git-upload-pack` or `git-receive-pack`&lt;br/&gt;#[derive(serde::Deserialize)]&lt;br/&gt;#[serde(rename_all = &amp;#34;kebab-case&amp;#34;)]&lt;br/&gt;pub enum ServiceName {&lt;br/&gt;    GitUploadPack,&lt;br/&gt;    GitReceivePack,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Query parameter containing the service name (`?service=&amp;lt;string&amp;gt;`)&lt;br/&gt;#[derive(serde::Deserialize)]&lt;br/&gt;pub struct ServiceQuery {&lt;br/&gt;    /// The service name&lt;br/&gt;    pub service: ServiceName,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// An extractor that indecates what file to return its content, it&amp;#39;s used with&lt;br/&gt;/// `get_file_content` endpoint only.&lt;br/&gt;pub struct GitFilePath(pub PathBuf);&lt;br/&gt;&lt;br/&gt;impl ServiceName {&lt;br/&gt;    /// Gets the service name without the `git-` prefix&lt;br/&gt;    pub const fn name(&amp;amp;self) -&amp;gt; &amp;amp;&amp;#39;static str {&lt;br/&gt;        match self {&lt;br/&gt;            Self::GitUploadPack =&amp;gt; &amp;#34;upload-pack&amp;#34;,&lt;br/&gt;            Self::GitReceivePack =&amp;gt; &amp;#34;receive-pack&amp;#34;,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the pkt-line header of the service&lt;br/&gt;    pub const fn pkt_line_header(&amp;amp;self) -&amp;gt; &amp;amp;[u8] {&lt;br/&gt;        match self {&lt;br/&gt;            Self::GitUploadPack =&amp;gt; &amp;#34;001e# service=git-upload-pack\n0000&amp;#34;.as_bytes(),&lt;br/&gt;            Self::GitReceivePack =&amp;gt; &amp;#34;001f# service=git-receive-pack\n0000&amp;#34;.as_bytes(),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;S&amp;gt; FromRequestParts&amp;lt;S&amp;gt; for GitFilePath&lt;br/&gt;where&lt;br/&gt;    S: Send &#43; Sync,&lt;br/&gt;{&lt;br/&gt;    type Rejection = (StatusCode, &amp;amp;&amp;#39;static str);&lt;br/&gt;&lt;br/&gt;    async fn from_request_parts(parts: &amp;amp;mut Parts, _state: &amp;amp;S) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {&lt;br/&gt;        // Skip `/&amp;lt;npub&amp;gt;/repo.git/`&lt;br/&gt;        let path = PathBuf::from(&lt;br/&gt;            parts&lt;br/&gt;                .uri&lt;br/&gt;                .path()&lt;br/&gt;                .trim_matches(&amp;#39;/&amp;#39;)&lt;br/&gt;                .split(&amp;#39;/&amp;#39;)&lt;br/&gt;                .skip(2)&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                .join(&amp;#34;/&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        if path.components().any(|component| {&lt;br/&gt;            matches!(&lt;br/&gt;                component,&lt;br/&gt;                Component::CurDir | Component::ParentDir | Component::Prefix(..)&lt;br/&gt;            )&lt;br/&gt;        }) {&lt;br/&gt;            return Err((StatusCode::BAD_REQUEST, &amp;#34;Invalid path&amp;#34;));&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        if path.as_os_str() == &amp;#34;HEAD&amp;#34;&lt;br/&gt;            || path.starts_with(&amp;#34;objects/info/&amp;#34;)&lt;br/&gt;            || path.starts_with(&amp;#34;objects/pack/&amp;#34;)&lt;br/&gt;        {&lt;br/&gt;            Ok(Self(path.to_path_buf()))&lt;br/&gt;        } else {&lt;br/&gt;            Err((StatusCode::BAD_REQUEST, &amp;#34;Unknown path&amp;#34;))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;S&amp;gt; FromRequestParts&amp;lt;S&amp;gt; for PublicKeyAndRepoPath&lt;br/&gt;where&lt;br/&gt;    S: Send &#43; Sync,&lt;br/&gt;{&lt;br/&gt;    type Rejection = (StatusCode, String);&lt;br/&gt;&lt;br/&gt;    async fn from_request_parts(parts: &amp;amp;mut Parts, state: &amp;amp;S) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {&lt;br/&gt;        use axum::extract::Path;&lt;br/&gt;&lt;br/&gt;        let Path(mut pkey_and_repo) =&lt;br/&gt;            Path::&amp;lt;PublicKeyAndRepoPath&amp;gt;::from_request_parts(parts, state)&lt;br/&gt;                .await&lt;br/&gt;                .map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;&lt;br/&gt;&lt;br/&gt;        if pkey_and_repo.repo_name.ends_with(&amp;#34;.git&amp;#34;) {&lt;br/&gt;            pkey_and_repo.repo_name = pkey_and_repo.repo_name.trim_end_matches(&amp;#34;.git&amp;#34;).to_owned();&lt;br/&gt;            return Ok(pkey_and_repo);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Err((&lt;br/&gt;            StatusCode::BAD_REQUEST,&lt;br/&gt;            &amp;#34;The repository name must ends with `.git`&amp;#34;.to_owned(),&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:03Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsq3jzyy2t9en4fwj9pfw9u57clyzpumacylqn98y6shx8dl8rnvrgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvltuukp</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsq3jzyy2t9en4fwj9pfw9u57clyzpumacylqn98y6shx8dl8rnvrgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvltuukp" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{io, path::Path, process::Output, time::Duration};&lt;br/&gt;&lt;br/&gt;use tokio::{fs, process::Command};&lt;br/&gt;&lt;br/&gt;/// Type alias for `Result&amp;lt;T, GraspGitError&amp;gt;`&lt;br/&gt;type GitResult&amp;lt;T&amp;gt; = Result&amp;lt;T, GraspGitError&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// Grasp git command errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum GraspGitError {&lt;br/&gt;    #[error(&amp;#34;GRASP git IO error: {0}&amp;#34;)]&lt;br/&gt;    Io(#[from] io::Error),&lt;br/&gt;    #[error(&amp;#34;GRASP bare repo error: {0}&amp;#34;)]&lt;br/&gt;    BareRepo(String),&lt;br/&gt;    #[error(&amp;#34;GRASP git command timeout: `{0:?}`&amp;#34;)]&lt;br/&gt;    GitTimeout(Vec&amp;lt;String&amp;gt;),&lt;br/&gt;    #[error(&amp;#34;GRASP error, repo doesn&amp;#39;t exists: {0}&amp;#34;)]&lt;br/&gt;    RepoNotFound(String),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Git command implementation for GRASP operations&lt;br/&gt;pub struct GraspGitCommand&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// Path to the git executable&lt;br/&gt;    pub git_path:  &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// Path to the git repository where commands will be executed&lt;br/&gt;    pub repo_path: &amp;amp;&amp;#39;a Path,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;a&amp;gt; GraspGitCommand&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// Creates a new Git command.&lt;br/&gt;    pub fn new(git_path: &amp;amp;&amp;#39;a str, repo_path: &amp;amp;&amp;#39;a Path) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            git_path,&lt;br/&gt;            repo_path,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the Git command with the provided arguments. Times out after 5&lt;br/&gt;    /// seconds.&lt;br/&gt;    async fn run_git(&amp;amp;self, args: &amp;amp;[&amp;amp;str]) -&amp;gt; GitResult&amp;lt;Output&amp;gt; {&lt;br/&gt;        if !self.repo_path.exists() {&lt;br/&gt;            tracing::trace!(&amp;#34;Creatring repo directory: {}&amp;#34;, self.repo_path.display());&lt;br/&gt;            fs::create_dir_all(self.repo_path).await?;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        tracing::trace!(&amp;#34;Run GRASP git command: {args:?}&amp;#34;);&lt;br/&gt;        tokio::time::timeout(&lt;br/&gt;            Duration::from_secs(5),&lt;br/&gt;            Command::new(self.git_path)&lt;br/&gt;                .kill_on_drop(true)&lt;br/&gt;                .current_dir(self.repo_path)&lt;br/&gt;                .args(args)&lt;br/&gt;                .output(),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .map_err(|_| GraspGitError::GitTimeout(args.iter().map(ToString::to_string).collect()))?&lt;br/&gt;        .map_err(GraspGitError::from)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Initializes a new bare Git repository.&lt;br/&gt;    pub async fn new_bare(&amp;amp;self, description: &amp;amp;str) -&amp;gt; GitResult&amp;lt;()&amp;gt; {&lt;br/&gt;        tracing::trace!(&amp;#34;Creatring bare git repo `{}`&amp;#34;, self.repo_path.display());&lt;br/&gt;        let output = self.run_git(&amp;amp;[&amp;#34;init&amp;#34;, &amp;#34;--bare&amp;#34;, &amp;#34;--quiet&amp;#34;, &amp;#34;.&amp;#34;]).await?;&lt;br/&gt;&lt;br/&gt;        if !output.status.success() {&lt;br/&gt;            return Err(GraspGitError::BareRepo(&lt;br/&gt;                String::from_utf8_lossy(&amp;amp;output.stderr).into_owned(),&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        self.update_description(description).await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Updates the repository&amp;#39;s description by writing the given string to the&lt;br/&gt;    /// &amp;#39;description&amp;#39; file. Returns an error if the repository path doesn&amp;#39;t&lt;br/&gt;    /// exist or if the write operation fails.&lt;br/&gt;    pub async fn update_description(&amp;amp;self, description: &amp;amp;str) -&amp;gt; GitResult&amp;lt;()&amp;gt; {&lt;br/&gt;        if !self.repo_path.exists() {&lt;br/&gt;            return Err(GraspGitError::RepoNotFound(&lt;br/&gt;                self.repo_path.display().to_string(),&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        tracing::trace!(&amp;#34;updating repo description: {}&amp;#34;, self.repo_path.display());&lt;br/&gt;        fs::write(self.repo_path.join(&amp;#34;description&amp;#34;), description)&lt;br/&gt;            .await&lt;br/&gt;            .map_err(GraspGitError::from)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:23:03Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsqfd5pfnfk04ftsahyjufp66szl4x7jvx5zj647pmspk75g0cjsjszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvjgw857</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsqfd5pfnfk04ftsahyjufp66szl4x7jvx5zj647pmspk75g0cjsjszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvjgw857" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use nostr::{event::Event, message::MachineReadablePrefix, util::BoxedFuture};&lt;br/&gt;use nostr_relay_builder::builder::WritePolicyResult;&lt;br/&gt;&lt;br/&gt;use crate::relay::plugins_manager::RelayPlugin;&lt;br/&gt;&lt;br/&gt;/// A plugin to check the event size.&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct EventSizePlugin(pub usize);&lt;br/&gt;&lt;br/&gt;impl RelayPlugin for EventSizePlugin {&lt;br/&gt;    fn check_event&amp;lt;&amp;#39;a&amp;gt;(&amp;amp;&amp;#39;a self, event: &amp;amp;&amp;#39;a Event) -&amp;gt; BoxedFuture&amp;lt;&amp;#39;a, Option&amp;lt;WritePolicyResult&amp;gt;&amp;gt; {&lt;br/&gt;        Box::pin(async move {&lt;br/&gt;            let event_size = event&lt;br/&gt;                .tags&lt;br/&gt;                .iter()&lt;br/&gt;                .flat_map(|t| t.as_slice())&lt;br/&gt;                .fold(0, |acc: usize, tag_content| {&lt;br/&gt;                    acc.saturating_add(tag_content.len())&lt;br/&gt;                })&lt;br/&gt;                .saturating_add(event.content.len());&lt;br/&gt;&lt;br/&gt;            if event_size &amp;gt; self.0 {&lt;br/&gt;                return Some(WritePolicyResult::reject(&lt;br/&gt;                    MachineReadablePrefix::Blocked,&lt;br/&gt;                    format!(&lt;br/&gt;                        &amp;#34;event size {event_size} is larger than maximum allowed {}&amp;#34;,&lt;br/&gt;                        self.0&lt;br/&gt;                    ),&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            None&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:53Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsgldz2ccpegz755mgcdxjmn6upya6pcfsc02cs4s6hahruvj92x2szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfp5ewq</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsgldz2ccpegz755mgcdxjmn6upya6pcfsc02cs4s6hahruvj92x2szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfp5ewq" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::time::Duration;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Router,&lt;br/&gt;    http::HeaderValue,&lt;br/&gt;    routing::{get, post},&lt;br/&gt;};&lt;br/&gt;use tower::limit::ConcurrencyLimitLayer;&lt;br/&gt;use tower_http::timeout::TimeoutLayer;&lt;br/&gt;&lt;br/&gt;use crate::relay_config::RelayConfig;&lt;br/&gt;&lt;br/&gt;/// Git endpoint to return a file content&lt;br/&gt;mod get_file_content;&lt;br/&gt;/// Git command&lt;br/&gt;pub mod git_command;&lt;br/&gt;/// Git `info/refs` endpoint&lt;br/&gt;mod info_refs;&lt;br/&gt;/// Path and query params&lt;br/&gt;mod path_and_query;&lt;br/&gt;/// Git receive pack endpoint&lt;br/&gt;mod receive_pack;&lt;br/&gt;/// Parses the ref update packet lines&lt;br/&gt;pub mod ref_update_pkt_line;&lt;br/&gt;/// Git upload pack endpoint&lt;br/&gt;mod upload_pack;&lt;br/&gt;/// Git utils&lt;br/&gt;pub mod utils;&lt;br/&gt;&lt;br/&gt;use get_file_content::get_file_content;&lt;br/&gt;use info_refs::info_refs;&lt;br/&gt;pub use path_and_query::*;&lt;br/&gt;use receive_pack::receive_pack;&lt;br/&gt;use upload_pack::upload_pack;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// The `Expires` header value to prevent caching (sets date far in the past)&lt;br/&gt;const EXPIRES_NO_CACHE: HeaderValue = HeaderValue::from_static(&amp;#34;Fri, 01 Jan 1980 00:00:00 GMT&amp;#34;);&lt;br/&gt;/// The `Cache-Control` header value to prevent caching&lt;br/&gt;const CACHE_CONTROL_NO_CACHE: HeaderValue =&lt;br/&gt;    HeaderValue::from_static(&amp;#34;no-cache, max-age=0, must-revalidate&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// Creates a router with paths prefixed by `/{public_key}/{repo_name}`.&lt;br/&gt;///&lt;br/&gt;/// # Example&lt;br/&gt;/// ```rust&lt;br/&gt;/// use axum::routing::get;&lt;br/&gt;/// use axum::Router;&lt;br/&gt;/// use n34_relay::git_router;&lt;br/&gt;///&lt;br/&gt;/// # fn main() {&lt;br/&gt;/// let router: Router&amp;lt;()&amp;gt; = git_router!(&lt;br/&gt;///     &amp;#34;/git-upload-pack&amp;#34; =&amp;gt; get::&amp;lt;_, ((),), ()&amp;gt;(|| async {})&lt;br/&gt;///     &amp;#34;/git-receive-pack&amp;#34; =&amp;gt; get::&amp;lt;_, ((),), ()&amp;gt;(|| async {})&lt;br/&gt;/// );&lt;br/&gt;/// # }&lt;br/&gt;/// ```&lt;br/&gt;#[macro_export]&lt;br/&gt;macro_rules! git_router {&lt;br/&gt;    ($($path:tt =&amp;gt; $endpoint:expr)&#43;) =&amp;gt; {&lt;br/&gt;            axum::Router::new()&lt;br/&gt;        $(&lt;br/&gt;            .route(const { const_format::concatcp!(&amp;#34;/{public_key}/{repo_name}&amp;#34;, $path) }, $endpoint)&lt;br/&gt;        )&#43;&lt;br/&gt;    };&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a router for git-related endpoints&lt;br/&gt;#[allow(deprecated)]&lt;br/&gt;pub fn router(config: &amp;amp;RelayConfig) -&amp;gt; Router {&lt;br/&gt;    let mut router = git_router!(&lt;br/&gt;        &amp;#34;/git-upload-pack&amp;#34; =&amp;gt; post(upload_pack)&lt;br/&gt;        &amp;#34;/git-receive-pack&amp;#34; =&amp;gt; post(receive_pack)&lt;br/&gt;        &amp;#34;/info/refs&amp;#34; =&amp;gt; get(info_refs)&lt;br/&gt;        &amp;#34;/HEAD&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;false&amp;gt;)&lt;br/&gt;        &amp;#34;/objects/info/packs&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;true&amp;gt;)&lt;br/&gt;        &amp;#34;/objects/info/{*rest}&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;false&amp;gt;)&lt;br/&gt;        &amp;#34;/objects/pack/{pack}&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;true&amp;gt;)&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    if let Some(max_reqs) = config.grasp.max_reqs {&lt;br/&gt;        router = router.layer(ConcurrencyLimitLayer::new(max_reqs.into()));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if let Some(timeout) = config.grasp.req_timeout {&lt;br/&gt;        router = router.layer(TimeoutLayer::new(Duration::from_secs(timeout.into())));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    router&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:52Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs0ww2hq6xanmh6cdxsvkwvej48z2zflrsyl54vs6svjtrulhkvlmczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsve2lkyt</id>
    
      <title type="html">// Copyright 2025 Thomas Profelt // // Permission is hereby ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs0ww2hq6xanmh6cdxsvkwvej48z2zflrsyl54vs6svjtrulhkvlmczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsve2lkyt" />
    <content type="html">
      // Copyright 2025 Thomas Profelt&lt;br/&gt;//&lt;br/&gt;// Permission is hereby granted, free of charge, to any person obtaining a copy&lt;br/&gt;// of this software and associated documentation files (the “Software”), to deal&lt;br/&gt;// in the Software without restriction, including without limitation the rights&lt;br/&gt;// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell&lt;br/&gt;// copies of the Software, and to permit persons to whom the Software is&lt;br/&gt;// furnished to do so, subject to the following conditions:&lt;br/&gt;//&lt;br/&gt;// The above copyright notice and this permission notice shall be included in&lt;br/&gt;// all copies or substantial portions of the Software.&lt;br/&gt;//&lt;br/&gt;// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR&lt;br/&gt;// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,&lt;br/&gt;// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE&lt;br/&gt;// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER&lt;br/&gt;// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,&lt;br/&gt;// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE&lt;br/&gt;// SOFTWARE.&lt;br/&gt;&lt;br/&gt;// This implementation is derived from the original work authored by Thomas&lt;br/&gt;// Profelt in the `axum-raw-websocket` library.&lt;br/&gt;//&lt;br/&gt;// Source: &lt;a href=&#34;https://github.com/tompro/axum-raw-websocket/blob/master/src/lib.rs&#34;&gt;https://github.com/tompro/axum-raw-websocket/blob/master/src/lib.rs&lt;/a&gt;&lt;br/&gt;// Licensed under the MIT License.&lt;br/&gt;//&lt;br/&gt;// Note: Only this specific file is licensed under MIT. The remainder of the&lt;br/&gt;// project is licensed under AGPLv3.&lt;br/&gt;&lt;br/&gt;use std::future::Future;&lt;br/&gt;&lt;br/&gt;use axum::body::Body;&lt;br/&gt;use axum::extract::FromRequestParts;&lt;br/&gt;use axum::extract::ws::rejection::InvalidProtocolPseudoheader;&lt;br/&gt;use axum::extract::ws::rejection::{&lt;br/&gt;    ConnectionNotUpgradable,&lt;br/&gt;    InvalidConnectionHeader,&lt;br/&gt;    InvalidUpgradeHeader,&lt;br/&gt;    InvalidWebSocketVersionHeader,&lt;br/&gt;    MethodNotConnect,&lt;br/&gt;    MethodNotGet,&lt;br/&gt;    WebSocketKeyHeaderMissing,&lt;br/&gt;    WebSocketUpgradeRejection,&lt;br/&gt;};&lt;br/&gt;use axum::http::{&lt;br/&gt;    Method,&lt;br/&gt;    StatusCode,&lt;br/&gt;    Version,&lt;br/&gt;    header::{self, HeaderMap, HeaderName, HeaderValue},&lt;br/&gt;    request::Parts,&lt;br/&gt;};&lt;br/&gt;use axum::{Error, body::Bytes, response::Response};&lt;br/&gt;use hyper::upgrade::Upgraded;&lt;br/&gt;use hyper_util::rt::TokioIo;&lt;br/&gt;use sha1::{Digest, Sha1};&lt;br/&gt;&lt;br/&gt;/// This websocket upgrade is based on the axum integrated one&lt;br/&gt;/// [WebSocketUpgrade](axum::extract::ws::WebSocketUpgrade).&lt;br/&gt;///&lt;br/&gt;/// The main difference is that it will onvoke the on_upgrade callback with the&lt;br/&gt;/// raw socket which allow the socket to be used by other libraries than the&lt;br/&gt;/// default tokio-tungstenite.&lt;br/&gt;///&lt;br/&gt;/// For HTTP/1.1 requests, this extractor requires the request method to be&lt;br/&gt;/// `GET`; in later versions, `CONNECT` is used instead.&lt;br/&gt;/// To support both, it should be used with [`any`](axum::routing::any).&lt;br/&gt;///&lt;br/&gt;/// See the [module docs](axum::extract::ws) for an example.&lt;br/&gt;///&lt;br/&gt;/// [`MethodFilter`]: axum::routing::MethodFilter&lt;br/&gt;#[cfg_attr(docsrs, doc(cfg(feature = &amp;#34;ws&amp;#34;)))]&lt;br/&gt;pub struct RawSocketUpgrade&amp;lt;F = DefaultOnFailedUpgrade&amp;gt; {&lt;br/&gt;    /// `None` if HTTP/2&#43; WebSockets are used.&lt;br/&gt;    sec_websocket_key:      Option&amp;lt;HeaderValue&amp;gt;,&lt;br/&gt;    on_upgrade:             hyper::upgrade::OnUpgrade,&lt;br/&gt;    on_failed_upgrade:      F,&lt;br/&gt;    sec_websocket_protocol: Option&amp;lt;HeaderValue&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;F&amp;gt; std::fmt::Debug for RawSocketUpgrade&amp;lt;F&amp;gt; {&lt;br/&gt;    fn fmt(&amp;amp;self, f: &amp;amp;mut std::fmt::Formatter&amp;lt;&amp;#39;_&amp;gt;) -&amp;gt; std::fmt::Result {&lt;br/&gt;        f.debug_struct(&amp;#34;RelayUpgrade&amp;#34;)&lt;br/&gt;            .field(&amp;#34;sec_websocket_key&amp;#34;, &amp;amp;self.sec_websocket_key)&lt;br/&gt;            .field(&amp;#34;sec_websocket_protocol&amp;#34;, &amp;amp;self.sec_websocket_protocol)&lt;br/&gt;            .finish_non_exhaustive()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;F&amp;gt; RawSocketUpgrade&amp;lt;F&amp;gt; {&lt;br/&gt;    #[allow(dead_code)]&lt;br/&gt;    pub fn on_failed_upgrade&amp;lt;C&amp;gt;(self, callback: C) -&amp;gt; RawSocketUpgrade&amp;lt;C&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        C: OnFailedUpgrade,&lt;br/&gt;    {&lt;br/&gt;        RawSocketUpgrade {&lt;br/&gt;            sec_websocket_key:      self.sec_websocket_key,&lt;br/&gt;            on_upgrade:             self.on_upgrade,&lt;br/&gt;            on_failed_upgrade:      callback,&lt;br/&gt;            sec_websocket_protocol: self.sec_websocket_protocol,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Finalize upgrading the connection and call the provided callback with&lt;br/&gt;    /// the stream.&lt;br/&gt;    #[must_use = &amp;#34;to set up the WebSocket connection, this response must be returned&amp;#34;]&lt;br/&gt;    pub fn on_upgrade&amp;lt;C, Fut&amp;gt;(self, callback: C) -&amp;gt; Response&lt;br/&gt;    where&lt;br/&gt;        C: FnOnce(TokioIo&amp;lt;Upgraded&amp;gt;) -&amp;gt; Fut &#43; Send &#43; &amp;#39;static,&lt;br/&gt;        Fut: Future&amp;lt;Output = ()&amp;gt; &#43; Send &#43; &amp;#39;static,&lt;br/&gt;        F: OnFailedUpgrade,&lt;br/&gt;    {&lt;br/&gt;        let on_upgrade = self.on_upgrade;&lt;br/&gt;        let on_failed_upgrade = self.on_failed_upgrade;&lt;br/&gt;&lt;br/&gt;        tokio::spawn(async move {&lt;br/&gt;            let upgraded = match on_upgrade.await {&lt;br/&gt;                Ok(upgraded) =&amp;gt; upgraded,&lt;br/&gt;                Err(err) =&amp;gt; {&lt;br/&gt;                    on_failed_upgrade.call(Error::new(err));&lt;br/&gt;                    return;&lt;br/&gt;                }&lt;br/&gt;            };&lt;br/&gt;            let upgraded: TokioIo&amp;lt;Upgraded&amp;gt; = TokioIo::new(upgraded);&lt;br/&gt;            callback(upgraded).await;&lt;br/&gt;        });&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;        if let Some(sec_websocket_key) = &amp;amp;self.sec_websocket_key {&lt;br/&gt;            // If `sec_websocket_key` was `Some`, we are using HTTP/1.1.&lt;br/&gt;&lt;br/&gt;            #[allow(clippy::declare_interior_mutable_const)]&lt;br/&gt;            const UPGRADE: HeaderValue = HeaderValue::from_static(&amp;#34;upgrade&amp;#34;);&lt;br/&gt;            #[allow(clippy::declare_interior_mutable_const)]&lt;br/&gt;            const WEBSOCKET: HeaderValue = HeaderValue::from_static(&amp;#34;websocket&amp;#34;);&lt;br/&gt;&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::SWITCHING_PROTOCOLS)&lt;br/&gt;                .header(header::CONNECTION, UPGRADE)&lt;br/&gt;                .header(header::UPGRADE, WEBSOCKET)&lt;br/&gt;                .header(&lt;br/&gt;                    header::SEC_WEBSOCKET_ACCEPT,&lt;br/&gt;                    sign(sec_websocket_key.as_bytes()),&lt;br/&gt;                )&lt;br/&gt;                .body(Body::empty())&lt;br/&gt;                .unwrap()&lt;br/&gt;        } else {&lt;br/&gt;            Response::new(Body::empty())&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// What to do when a connection upgrade fails.&lt;br/&gt;///&lt;br/&gt;/// See [`RawSocketUpgrade::on_failed_upgrade`] for more details.&lt;br/&gt;pub trait OnFailedUpgrade: Send &#43; &amp;#39;static {&lt;br/&gt;    /// Call the callback.&lt;br/&gt;    fn call(self, error: Error);&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;F&amp;gt; OnFailedUpgrade for F&lt;br/&gt;where&lt;br/&gt;    F: FnOnce(Error) &#43; Send &#43; &amp;#39;static,&lt;br/&gt;{&lt;br/&gt;    fn call(self, error: Error) {&lt;br/&gt;        self(error)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// The default `OnFailedUpgrade` used by `RawSocketUpgrade`.&lt;br/&gt;///&lt;br/&gt;/// It simply ignores the error.&lt;br/&gt;#[non_exhaustive]&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct DefaultOnFailedUpgrade;&lt;br/&gt;&lt;br/&gt;impl OnFailedUpgrade for DefaultOnFailedUpgrade {&lt;br/&gt;    #[inline]&lt;br/&gt;    fn call(self, _error: Error) {}&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;S&amp;gt; FromRequestParts&amp;lt;S&amp;gt; for RawSocketUpgrade&amp;lt;DefaultOnFailedUpgrade&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Send &#43; Sync,&lt;br/&gt;{&lt;br/&gt;    type Rejection = WebSocketUpgradeRejection;&lt;br/&gt;&lt;br/&gt;    async fn from_request_parts(parts: &amp;amp;mut Parts, _state: &amp;amp;S) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {&lt;br/&gt;        let sec_websocket_key = if parts.version &amp;lt;= Version::HTTP_11 {&lt;br/&gt;            if parts.method != Method::GET {&lt;br/&gt;                return Err(WebSocketUpgradeRejection::MethodNotGet(&lt;br/&gt;                    MethodNotGet::default(),&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if !header_contains(&amp;amp;parts.headers, header::CONNECTION, &amp;#34;upgrade&amp;#34;) {&lt;br/&gt;                return Err(WebSocketUpgradeRejection::InvalidConnectionHeader(&lt;br/&gt;                    InvalidConnectionHeader::default(),&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if !header_eq(&amp;amp;parts.headers, header::UPGRADE, &amp;#34;websocket&amp;#34;) {&lt;br/&gt;                return Err(WebSocketUpgradeRejection::InvalidUpgradeHeader(&lt;br/&gt;                    InvalidUpgradeHeader::default(),&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            Some(&lt;br/&gt;                parts&lt;br/&gt;                    .headers&lt;br/&gt;                    .get(header::SEC_WEBSOCKET_KEY)&lt;br/&gt;                    .ok_or(WebSocketUpgradeRejection::WebSocketKeyHeaderMissing(&lt;br/&gt;                        WebSocketKeyHeaderMissing::default(),&lt;br/&gt;                    ))?&lt;br/&gt;                    .clone(),&lt;br/&gt;            )&lt;br/&gt;        } else {&lt;br/&gt;            if parts.method != Method::CONNECT {&lt;br/&gt;                return Err(WebSocketUpgradeRejection::MethodNotConnect(&lt;br/&gt;                    MethodNotConnect::default(),&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            if parts&lt;br/&gt;                .extensions&lt;br/&gt;                .get::&amp;lt;hyper::ext::Protocol&amp;gt;()&lt;br/&gt;                .is_none_or(|p| p.as_str() != &amp;#34;websocket&amp;#34;)&lt;br/&gt;            {&lt;br/&gt;                return Err(WebSocketUpgradeRejection::InvalidProtocolPseudoheader(&lt;br/&gt;                    InvalidProtocolPseudoheader::default(),&lt;br/&gt;                ));&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            None&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        if !header_eq(&amp;amp;parts.headers, header::SEC_WEBSOCKET_VERSION, &amp;#34;13&amp;#34;) {&lt;br/&gt;            return Err(WebSocketUpgradeRejection::InvalidWebSocketVersionHeader(&lt;br/&gt;                InvalidWebSocketVersionHeader::default(),&lt;br/&gt;            ));&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        let on_upgrade = parts&lt;br/&gt;            .extensions&lt;br/&gt;            .remove::&amp;lt;hyper::upgrade::OnUpgrade&amp;gt;()&lt;br/&gt;            .ok_or(WebSocketUpgradeRejection::ConnectionNotUpgradable(&lt;br/&gt;                ConnectionNotUpgradable::default(),&lt;br/&gt;            ))?;&lt;br/&gt;&lt;br/&gt;        let sec_websocket_protocol = parts.headers.get(header::SEC_WEBSOCKET_PROTOCOL).cloned();&lt;br/&gt;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            sec_websocket_key,&lt;br/&gt;            on_upgrade,&lt;br/&gt;            sec_websocket_protocol,&lt;br/&gt;            on_failed_upgrade: DefaultOnFailedUpgrade,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn header_eq(headers: &amp;amp;HeaderMap, key: HeaderName, value: &amp;amp;&amp;#39;static str) -&amp;gt; bool {&lt;br/&gt;    if let Some(header) = headers.get(&amp;amp;key) {&lt;br/&gt;        header.as_bytes().eq_ignore_ascii_case(value.as_bytes())&lt;br/&gt;    } else {&lt;br/&gt;        false&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn header_contains(headers: &amp;amp;HeaderMap, key: HeaderName, value: &amp;amp;&amp;#39;static str) -&amp;gt; bool {&lt;br/&gt;    let header = if let Some(header) = headers.get(&amp;amp;key) {&lt;br/&gt;        header&lt;br/&gt;    } else {&lt;br/&gt;        return false;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    if let Ok(header) = std::str::from_utf8(header.as_bytes()) {&lt;br/&gt;        header.to_ascii_lowercase().contains(value)&lt;br/&gt;    } else {&lt;br/&gt;        false&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn sign(key: &amp;amp;[u8]) -&amp;gt; HeaderValue {&lt;br/&gt;    use base64::engine::Engine as _;&lt;br/&gt;&lt;br/&gt;    let mut sha1 = Sha1::default();&lt;br/&gt;    sha1.update(key);&lt;br/&gt;    sha1.update(b&amp;#34;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&amp;#34;);&lt;br/&gt;    let b64 = Bytes::from(base64::engine::general_purpose::STANDARD.encode(sha1.finalize()));&lt;br/&gt;    HeaderValue::from_maybe_shared(b64).expect(&amp;#34;base64 is a valid value&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:42Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsty58has5dpa4ste5skfmlw4wdhhv77sf2wa9rcp54uxhwxd2kepczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhezlr5</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsty58has5dpa4ste5skfmlw4wdhhv77sf2wa9rcp54uxhwxd2kepczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhezlr5" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    extract::Query,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::{HeaderMap, StatusCode};&lt;br/&gt;&lt;br/&gt;use super::{ServiceQuery, git_command::GitCommand};&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;/// Retrieves Git repository reference information for a given service.&lt;br/&gt;/// Returns a response containing the references or an appropriate error message&lt;br/&gt;/// if the repository or service is not found.&lt;br/&gt;pub async fn info_refs(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    Query(ServiceQuery { service }): Query&amp;lt;ServiceQuery&amp;gt;,&lt;br/&gt;    headers: HeaderMap,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    match GitCommand::new(&amp;amp;state.config.grasp.git_path, &amp;amp;repo_path)&lt;br/&gt;        .refs(&amp;amp;service, super::utils::contains_git_v2(&amp;amp;headers))&lt;br/&gt;        .await&lt;br/&gt;    {&lt;br/&gt;        Ok(response_body) =&amp;gt; {&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::OK)&lt;br/&gt;                .header(&lt;br/&gt;                    &amp;#34;Content-Type&amp;#34;,&lt;br/&gt;                    format!(&amp;#34;application/x-git-{}-advertisement&amp;#34;, service.name()),&lt;br/&gt;                )&lt;br/&gt;                .header(&amp;#34;Pragma&amp;#34;, &amp;#34;no-cache&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Cache-Control&amp;#34;, crate::git_server::CACHE_CONTROL_NO_CACHE)&lt;br/&gt;                .header(&amp;#34;Expires&amp;#34;, crate::git_server::EXPIRES_NO_CACHE)&lt;br/&gt;                .body(response_body)&lt;br/&gt;                .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;        Err(err_msg) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, err_msg).into_response(),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:38Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxx4vnr2xfts8gvd88c8f0zh0hexw5v0gcw0kmkahss3r5scap8lgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvkhjelf</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxx4vnr2xfts8gvd88c8f0zh0hexw5v0gcw0kmkahss3r5scap8lgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvkhjelf" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::PathBuf;&lt;br/&gt;use std::env;&lt;br/&gt;&lt;br/&gt;const BASE_DIR_ENV_VAR: &amp;amp;str = &amp;#34;N34_RELAY_BASE_DIR&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Returns the base directory for n34-relay.&lt;br/&gt;/// Priority:&lt;br/&gt;/// 1. Environment variable `N34_RELAY_BASE_DIR`&lt;br/&gt;/// 2. OS-specific config directory &#43; `/n34-relay`&lt;br/&gt;/// 3. Fallback to current directory if config_dir is unavailable&lt;br/&gt;pub fn get_base_dir() -&amp;gt; PathBuf {&lt;br/&gt;    if let Ok(env_path) = env::var(BASE_DIR_ENV_VAR) {&lt;br/&gt;        return PathBuf::from(env_path);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Linux:   /home/alice/.config/n34-relay&lt;br/&gt;    // macOS:   /Users/alice/Library/Application Support/n34-relay&lt;br/&gt;    // Windows: C:\Users\Alice\AppData\Roaming\n34-relay&lt;br/&gt;    dirs::config_dir()&lt;br/&gt;        .map(|path| path.join(&amp;#34;n34-relay&amp;#34;))&lt;br/&gt;        .unwrap_or_else(|| PathBuf::from(&amp;#34;.&amp;#34;))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Alias for get_base_dir to fix E0425 errors in downstream functions&lt;br/&gt;pub fn base_dir_path() -&amp;gt; PathBuf {&lt;br/&gt;    get_base_dir()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Helper for the default config file path&lt;br/&gt;pub fn get_default_config_path() -&amp;gt; PathBuf {&lt;br/&gt;    base_dir_path().join(&amp;#34;config.toml&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn config_file_path() -&amp;gt; PathBuf {&lt;br/&gt;    get_default_config_path()&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn lmdb_dir_path() -&amp;gt; PathBuf {&lt;br/&gt;    base_dir_path().join(&amp;#34;lmdb&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn logs_file_path() -&amp;gt; PathBuf {&lt;br/&gt;    base_dir_path().join(&amp;#34;logs.log&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn homepage_file_path() -&amp;gt; PathBuf {&lt;br/&gt;    base_dir_path().join(&amp;#34;homepage.html&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn rhai_plugins_dir() -&amp;gt; PathBuf {&lt;br/&gt;    base_dir_path().join(&amp;#34;rhai-plugins&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub fn grasp_repos() -&amp;gt; PathBuf {&lt;br/&gt;    base_dir_path().join(&amp;#34;repos&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:31Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsd2qq9je9506l900k3d7sexgxlx2efdf69tw53ayk78tjdpn800eszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4kng98</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsd2qq9je9506l900k3d7sexgxlx2efdf69tw53ayk78tjdpn800eszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4kng98" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{path::Path, process::Stdio};&lt;br/&gt;&lt;br/&gt;use axum::body::Body;&lt;br/&gt;use tokio::{&lt;br/&gt;    io::{AsyncReadExt, AsyncWriteExt},&lt;br/&gt;    process::{Child, ChildStderr, Command},&lt;br/&gt;};&lt;br/&gt;use tokio_util::io::ReaderStream;&lt;br/&gt;&lt;br/&gt;use crate::git_server::ServiceName;&lt;br/&gt;&lt;br/&gt;/// A helper for executing git commands with specific paths.&lt;br/&gt;pub struct GitCommand&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    git_path: &amp;amp;&amp;#39;a str,&lt;br/&gt;    git_repo: &amp;amp;&amp;#39;a Path,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;a&amp;gt; GitCommand&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// Creates a new [`GitCommand`] instance with the given git path and&lt;br/&gt;    /// repository.&lt;br/&gt;    pub fn new(git_path: &amp;amp;&amp;#39;a str, git_repo: &amp;amp;&amp;#39;a Path) -&amp;gt; Self {&lt;br/&gt;        Self { git_path, git_repo }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Spawns a git process with the provided arguments.&lt;br/&gt;    fn spawn_git(&amp;amp;self, args: &amp;amp;[&amp;amp;str], v2: bool) -&amp;gt; Option&amp;lt;Child&amp;gt; {&lt;br/&gt;        let mut command = Command::new(self.git_path);&lt;br/&gt;        // from GRASP protocol:&lt;br/&gt;        // MUST include `allow-reachable-sha1-in-want` and&lt;br/&gt;        // `allow-tip-sha1-in-want` in advertisement and serve available oids.&lt;br/&gt;        command.args([&amp;#34;-c&amp;#34;, &amp;#34;uploadpack.allowTipSHA1InWant=true&amp;#34;]);&lt;br/&gt;        command.args([&amp;#34;-c&amp;#34;, &amp;#34;uploadpack.allowReachableSHA1InWant=true&amp;#34;]);&lt;br/&gt;&lt;br/&gt;        command.args(args);&lt;br/&gt;        command.stdin(Stdio::piped());&lt;br/&gt;        command.stdout(Stdio::piped());&lt;br/&gt;        command.stderr(Stdio::piped());&lt;br/&gt;        command.current_dir(self.git_repo);&lt;br/&gt;&lt;br/&gt;        if v2 {&lt;br/&gt;            command.env(&amp;#34;GIT_PROTOCOL&amp;#34;, &amp;#34;version=2&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        command.spawn().ok()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Sends a request body to a git command and returns its output as a&lt;br/&gt;    /// stream.&lt;br/&gt;    async fn pass_request_to_git(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        args: &amp;amp;[&amp;amp;&amp;#39;static str],&lt;br/&gt;        body: &amp;amp;[u8],&lt;br/&gt;        v2: bool,&lt;br/&gt;    ) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let Some(mut process) = self.spawn_git(args, v2) else {&lt;br/&gt;            return Err(&amp;#34;Failed to run git command&amp;#34;);&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let mut stdin = process.stdin.take().expect(&amp;#34;git stdin&amp;#34;);&lt;br/&gt;        if let Err(err) = stdin.write_all(body).await {&lt;br/&gt;            tracing::error!(args = ?args, error = %err, &amp;#34;Failed to write the request body to git stdin&amp;#34;);&lt;br/&gt;            return Err(&amp;#34;Failed to pass the request body to git&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        drop(stdin);&lt;br/&gt;&lt;br/&gt;        let stderr = process.stderr.take().expect(&amp;#34;git stderr&amp;#34;);&lt;br/&gt;        let stdout = process.stdout.take().expect(&amp;#34;git stdout&amp;#34;);&lt;br/&gt;        wait_process(process, stderr);&lt;br/&gt;        Ok(Body::from_stream(ReaderStream::new(stdout)))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes a git command with the provided arguments and returns its&lt;br/&gt;    /// output. If a body is needed, use [Self::pass_request_to_git]&lt;br/&gt;    /// instead.&lt;br/&gt;    async fn call_git(&amp;amp;self, args: &amp;amp;[&amp;amp;&amp;#39;static str], v2: bool) -&amp;gt; Result&amp;lt;Vec&amp;lt;u8&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let Some(process) = self.spawn_git(args, v2) else {&lt;br/&gt;            return Err(&amp;#34;Failed to run git command&amp;#34;);&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        process&lt;br/&gt;            .wait_with_output()&lt;br/&gt;            .await&lt;br/&gt;            .map(|o| o.stdout)&lt;br/&gt;            .map_err(|err| {&lt;br/&gt;                tracing::error!(args = ?args, error = %err, &amp;#34;Failed to get `git` output&amp;#34;);&lt;br/&gt;                &amp;#34;Failed to run `git` command&amp;#34;&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the `receive-pack` git command with the provided body.&lt;br/&gt;    pub async fn receive_pack(&amp;amp;self, body: &amp;amp;[u8]) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        self.pass_request_to_git(&amp;amp;[&amp;#34;receive-pack&amp;#34;, &amp;#34;--stateless-rpc&amp;#34;, &amp;#34;.&amp;#34;], body, false)&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the `upload-pack` git command with the provided body.&lt;br/&gt;    pub async fn upload_pack(&amp;amp;self, body: &amp;amp;[u8], v2: bool) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        self.pass_request_to_git(&amp;amp;[&amp;#34;upload-pack&amp;#34;, &amp;#34;--stateless-rpc&amp;#34;, &amp;#34;.&amp;#34;], body, v2)&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the given service git command with `--advertise-refs` argument.&lt;br/&gt;    pub async fn refs(&amp;amp;self, service: &amp;amp;ServiceName, v2: bool) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let body_bytes = self&lt;br/&gt;            .call_git(&lt;br/&gt;                &amp;amp;[service.name(), &amp;#34;--stateless-rpc&amp;#34;, &amp;#34;--advertise-refs&amp;#34;, &amp;#34;.&amp;#34;],&lt;br/&gt;                v2,&lt;br/&gt;            )&lt;br/&gt;            .await?;&lt;br/&gt;&lt;br/&gt;        Ok(Body::from(&lt;br/&gt;            [service.pkt_line_header(), body_bytes.as_ref()].concat(),&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Spawns a task to wait for the process to complete and logs any errors.&lt;br/&gt;///&lt;br/&gt;/// If the process exits with non-zero status, reads and logs the stderr output.&lt;br/&gt;fn wait_process(mut process: Child, mut stderr: ChildStderr) {&lt;br/&gt;    tokio::spawn(async move {&lt;br/&gt;        let pid = process.id().unwrap_or_default();&lt;br/&gt;        match process.wait().await {&lt;br/&gt;            Ok(status) =&amp;gt; {&lt;br/&gt;                if !status.success() {&lt;br/&gt;                    let mut err = String::new();&lt;br/&gt;                    _ = stderr.read_to_string(&amp;amp;mut err).await;&lt;br/&gt;                    tracing::warn!(&lt;br/&gt;                        &amp;#34;Git process (PID {pid}) exited with non-zero status: {} ({status})&amp;#34;,&lt;br/&gt;                        err.trim(),&lt;br/&gt;                    );&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            Err(e) =&amp;gt; {&lt;br/&gt;                tracing::error!(&amp;#34;Error waiting for Git process (PID {pid}): {e}&amp;#34;);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    });&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:27Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqszxrqnj5rvml5fwe9ccfhp70y262zaascf7r5r4qf8djzcgzjad7szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyzl50d</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqszxrqnj5rvml5fwe9ccfhp70y262zaascf7r5r4qf8djzcgzjad7szyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyzl50d" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::SocketAddr, process::ExitCode, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::Extension;&lt;br/&gt;use hyper::{Method, header};&lt;br/&gt;use tokio::signal;&lt;br/&gt;use tower_http::{&lt;br/&gt;    cors,&lt;br/&gt;    decompression::RequestDecompressionLayer,&lt;br/&gt;    trace::{DefaultMakeSpan, TraceLayer},&lt;br/&gt;};&lt;br/&gt;use tracing::level_filters::LevelFilter;&lt;br/&gt;use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;/// Relay endpoints&lt;br/&gt;mod endpoints;&lt;br/&gt;/// Relay errors.&lt;br/&gt;mod errors;&lt;br/&gt;/// Extension traits&lt;br/&gt;mod ext_traits;&lt;br/&gt;/// GRASP git server&lt;br/&gt;mod git_server;&lt;br/&gt;/// Relay pathes.&lt;br/&gt;mod pathes;&lt;br/&gt;/// Raw axum websocket&lt;br/&gt;mod raw_websocket;&lt;br/&gt;/// Our relay.&lt;br/&gt;mod relay;&lt;br/&gt;/// Relay configuration.&lt;br/&gt;mod relay_config;&lt;br/&gt;/// Router state&lt;br/&gt;mod router_state;&lt;br/&gt;/// Some useful utils.&lt;br/&gt;mod utils;&lt;br/&gt;&lt;br/&gt;use self::{errors::RelayResult, relay_config::RelayConfig, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Sets up default logging with two outputs, stderr and a log file.&lt;br/&gt;///&lt;br/&gt;/// Log level for stderr is controlled by `RUST_LOG` environment variable,&lt;br/&gt;/// defaults to `ERROR`. The log file always uses `TRACE` level.&lt;br/&gt;fn setup_logs() -&amp;gt; errors::RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    tracing::subscriber::set_global_default(&lt;br/&gt;        tracing_subscriber::registry()&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(true)&lt;br/&gt;                    .with_writer(std::io::stderr)&lt;br/&gt;                    .without_time()&lt;br/&gt;                    .with_filter(EnvFilter::from_default_env()),&lt;br/&gt;            )&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(false)&lt;br/&gt;                    .with_writer(utils::logs_file()?)&lt;br/&gt;                    .with_file(false)&lt;br/&gt;                    .with_line_number(false)&lt;br/&gt;                    .with_filter(LevelFilter::TRACE),&lt;br/&gt;            ),&lt;br/&gt;    )&lt;br/&gt;    .ok();&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn shutdown_signal() {&lt;br/&gt;    let ctrl_c = async {&lt;br/&gt;        signal::ctrl_c()&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;Failed to install CTRL&#43;C handler&amp;#34;);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(unix)]&lt;br/&gt;    let terminate = async {&lt;br/&gt;        use tokio::signal::unix::SignalKind;&lt;br/&gt;&lt;br/&gt;        signal::unix::signal(SignalKind::terminate())&lt;br/&gt;            .expect(&amp;#34;Failed to create SIGTERM handler&amp;#34;)&lt;br/&gt;            .recv()&lt;br/&gt;            .await;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(not(unix))]&lt;br/&gt;    let terminate = std::future::pending::&amp;lt;()&amp;gt;();&lt;br/&gt;&lt;br/&gt;    tokio::select! {&lt;br/&gt;        _ = ctrl_c =&amp;gt; {},&lt;br/&gt;        _ = terminate =&amp;gt; {},&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;async fn try_main() -&amp;gt; RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    setup_logs()?;&lt;br/&gt;    let config = Arc::new(RelayConfig::reload()?);&lt;br/&gt;    let relay_db = config.get_relay_db().await?;&lt;br/&gt;    let n34_relay = Arc::new(relay::build_relay(Arc::clone(&amp;amp;config), Arc::clone(&amp;amp;relay_db)).await);&lt;br/&gt;    let addr = SocketAddr::new(config.net.ip, config.net.port);&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&amp;#34;Running relay with configuration: {config:#?}&amp;#34;);&lt;br/&gt;    tracing::info!(&amp;#34;Relay is running at `{}`&amp;#34;, n34_relay.url().await);&lt;br/&gt;&lt;br/&gt;    let mut app = axum::Router::new()&lt;br/&gt;        // main handler. GET and POST&lt;br/&gt;        .route(&amp;#34;/&amp;#34;, axum::routing::get(endpoints::main_handler).post(endpoints::main_handler));&lt;br/&gt;&lt;br/&gt;    if config.grasp.enable {&lt;br/&gt;        tracing::info!(&amp;#34;Git server is running&amp;#34;);&lt;br/&gt;        app = app.merge(git_server::router(&amp;amp;config));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    app = app&lt;br/&gt;        // enable cross-origin access&lt;br/&gt;        .route(&lt;br/&gt;            &amp;#34;/&amp;#34;,&lt;br/&gt;            axum::routing::options(|| async { hyper::StatusCode::NO_CONTENT }),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            cors::CorsLayer::new()&lt;br/&gt;                .allow_origin(cors::Any)&lt;br/&gt;                .allow_methods([Method::GET, Method::POST])&lt;br/&gt;                .allow_headers([header::CONTENT_TYPE]),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            TraceLayer::new_for_http()&lt;br/&gt;                .make_span_with(DefaultMakeSpan::default().include_headers(true)),&lt;br/&gt;        )&lt;br/&gt;        .layer(RequestDecompressionLayer::new())&lt;br/&gt;        .layer(Extension(Arc::new(RouterState::new(&lt;br/&gt;            config, n34_relay, relay_db,&lt;br/&gt;        ))));&lt;br/&gt;&lt;br/&gt;    axum::serve(&lt;br/&gt;        tokio::net::TcpListener::bind(addr).await?,&lt;br/&gt;        app.into_make_service_with_connect_info::&amp;lt;SocketAddr&amp;gt;(),&lt;br/&gt;    )&lt;br/&gt;    .with_graceful_shutdown(shutdown_signal())&lt;br/&gt;    .await?;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[tokio::main]&lt;br/&gt;async fn main() -&amp;gt; ExitCode {&lt;br/&gt;    if let Err(err) = try_main().await {&lt;br/&gt;        eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;        return ExitCode::FAILURE;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    tracing::info!(&amp;#34;Exited gracefully without any errors&amp;#34;);&lt;br/&gt;    ExitCode::SUCCESS&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:20Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsfy5dzt02dpv32ajaw597gaxfuqqwz37f5v4d68e0tl8zdw5mqtgszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvsn2l2</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsfy5dzt02dpv32ajaw597gaxfuqqwz37f5v4d68e0tl8zdw5mqtgszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvsn2l2" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    body::Body,&lt;br/&gt;    http::HeaderValue,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::{StatusCode, header};&lt;br/&gt;&lt;br/&gt;use crate::{git_server::GitFilePath, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Fetches the content of a file in a repository using the provided&lt;br/&gt;/// `GitFilePath`. The function checks if the repository and file exist, and&lt;br/&gt;/// handles caching headers based on the `CACHE_HEADER` flag.&lt;br/&gt;pub async fn get_file_content&amp;lt;const CACHE_HEADER: bool&amp;gt;(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    GitFilePath(file_path): GitFilePath,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let file_path = repo_path.join(file_path);&lt;br/&gt;&lt;br/&gt;    if !file_path.exists() {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;File not found&amp;#34;).into_response();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let mut response = Response::builder();&lt;br/&gt;    let headers = response&lt;br/&gt;        .headers_mut()&lt;br/&gt;        .expect(&amp;#34;builder function provide the response parts&amp;#34;);&lt;br/&gt;&lt;br/&gt;    if CACHE_HEADER {&lt;br/&gt;        let expires_value = (chrono::Utc::now() &#43; chrono::Duration::days(1)).to_rfc2822();&lt;br/&gt;        headers.insert(&lt;br/&gt;            &amp;#34;Cache-Control&amp;#34;,&lt;br/&gt;            HeaderValue::from_static(&amp;#34;public, max-age=86400&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;        headers.insert(&lt;br/&gt;            &amp;#34;Expires&amp;#34;,&lt;br/&gt;            HeaderValue::from_str(&amp;amp;expires_value).expect(&amp;#34;valid header value&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let content_type = if file_path.extension().is_some_and(|ext| ext == &amp;#34;pack&amp;#34;) {&lt;br/&gt;            &amp;#34;application/x-git-packed-objects&amp;#34;&lt;br/&gt;        } else if file_path.extension().is_some_and(|ext| ext == &amp;#34;idx&amp;#34;) {&lt;br/&gt;            &amp;#34;application/x-git-packed-objects-toc&amp;#34;&lt;br/&gt;        } else {&lt;br/&gt;            &amp;#34;application/x-git-loose-object&amp;#34;&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));&lt;br/&gt;    } else {&lt;br/&gt;        headers.insert(&amp;#34;Pragma&amp;#34;, HeaderValue::from_static(&amp;#34;no-cache&amp;#34;));&lt;br/&gt;        headers.insert(&amp;#34;Cache-Control&amp;#34;, crate::git_server::CACHE_CONTROL_NO_CACHE);&lt;br/&gt;        headers.insert(&amp;#34;Expires&amp;#34;, crate::git_server::EXPIRES_NO_CACHE);&lt;br/&gt;        headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(&amp;#34;text/plain&amp;#34;));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let Ok(file_content) = tokio::fs::read(&amp;amp;file_path).await else {&lt;br/&gt;        return (&lt;br/&gt;            StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;            &amp;#34;Failed to get file content&amp;#34;,&lt;br/&gt;        )&lt;br/&gt;            .into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    Response::builder()&lt;br/&gt;        .status(StatusCode::OK)&lt;br/&gt;        .body(Body::from(file_content))&lt;br/&gt;        .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:16Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsrp6pkdk0tfdaajurq5ndftspgq3aj4n49egcz3kcsr8evjt5akfqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvff47d3</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsrp6pkdk0tfdaajurq5ndftspgq3aj4n49egcz3kcsr8evjt5akfqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvff47d3" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{net::SocketAddr, /*process::ExitCode, */sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::Extension;&lt;br/&gt;use hyper::{Method, header};&lt;br/&gt;use tokio::signal;&lt;br/&gt;use tower_http::{&lt;br/&gt;    cors,&lt;br/&gt;    decompression::RequestDecompressionLayer,&lt;br/&gt;    trace::{DefaultMakeSpan, TraceLayer},&lt;br/&gt;};&lt;br/&gt;use tracing::level_filters::LevelFilter;&lt;br/&gt;use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt};&lt;br/&gt;&lt;br/&gt;/// Relay endpoints&lt;br/&gt;pub mod endpoints;&lt;br/&gt;/// Relay errors.&lt;br/&gt;pub mod errors;&lt;br/&gt;/// Extension traits&lt;br/&gt;pub mod ext_traits;&lt;br/&gt;/// GRASP git server&lt;br/&gt;pub mod git_server;&lt;br/&gt;/// Relay pathes.&lt;br/&gt;pub mod pathes;&lt;br/&gt;/// Raw axum websocket&lt;br/&gt;pub mod raw_websocket;&lt;br/&gt;/// Our relay.&lt;br/&gt;pub mod relay;&lt;br/&gt;/// Relay configuration.&lt;br/&gt;pub mod relay_config;&lt;br/&gt;/// Router state&lt;br/&gt;pub mod router_state;&lt;br/&gt;/// Some useful utils.&lt;br/&gt;pub mod utils;&lt;br/&gt;&lt;br/&gt;use self::{errors::RelayResult, relay_config::RelayConfig, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Sets up default logging with two outputs, stderr and a log file.&lt;br/&gt;///&lt;br/&gt;/// Log level for stderr is controlled by `RUST_LOG` environment variable,&lt;br/&gt;/// defaults to `ERROR`. The log file always uses `TRACE` level.&lt;br/&gt;pub fn setup_logs() -&amp;gt; errors::RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    tracing::subscriber::set_global_default(&lt;br/&gt;        tracing_subscriber::registry()&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(true)&lt;br/&gt;                    .with_writer(std::io::stderr)&lt;br/&gt;                    .without_time()&lt;br/&gt;                    .with_filter(EnvFilter::from_default_env()),&lt;br/&gt;            )&lt;br/&gt;            .with(&lt;br/&gt;                tracing_subscriber::fmt::layer()&lt;br/&gt;                    .with_ansi(false)&lt;br/&gt;                    .with_writer(utils::logs_file()?)&lt;br/&gt;                    .with_file(false)&lt;br/&gt;                    .with_line_number(false)&lt;br/&gt;                    .with_filter(LevelFilter::TRACE),&lt;br/&gt;            ),&lt;br/&gt;    )&lt;br/&gt;    .ok();&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub async fn shutdown_signal() {&lt;br/&gt;    let ctrl_c = async {&lt;br/&gt;        signal::ctrl_c()&lt;br/&gt;            .await&lt;br/&gt;            .expect(&amp;#34;Failed to install CTRL&#43;C handler&amp;#34;);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(unix)]&lt;br/&gt;    let terminate = async {&lt;br/&gt;        use tokio::signal::unix::SignalKind;&lt;br/&gt;&lt;br/&gt;        signal::unix::signal(SignalKind::terminate())&lt;br/&gt;            .expect(&amp;#34;Failed to create SIGTERM handler&amp;#34;)&lt;br/&gt;            .recv()&lt;br/&gt;            .await;&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    #[cfg(not(unix))]&lt;br/&gt;    let terminate = std::future::pending::&amp;lt;()&amp;gt;();&lt;br/&gt;&lt;br/&gt;    tokio::select! {&lt;br/&gt;        _ = ctrl_c =&amp;gt; {},&lt;br/&gt;        _ = terminate =&amp;gt; {},&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;pub async fn try_main() -&amp;gt; RelayResult&amp;lt;()&amp;gt; {&lt;br/&gt;    setup_logs()?;&lt;br/&gt;    let config = Arc::new(RelayConfig::reload()?);&lt;br/&gt;    let relay_db = config.get_relay_db().await?;&lt;br/&gt;    let n34_relay = Arc::new(relay::build_relay(Arc::clone(&amp;amp;config), Arc::clone(&amp;amp;relay_db)).await);&lt;br/&gt;    let addr = SocketAddr::new(config.net.ip, config.net.port);&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&amp;#34;Running relay with configuration: {config:#?}&amp;#34;);&lt;br/&gt;    tracing::info!(&amp;#34;Relay is running at `{}`&amp;#34;, n34_relay.url().await);&lt;br/&gt;&lt;br/&gt;    let mut app = axum::Router::new()&lt;br/&gt;        // main handler. GET and POST&lt;br/&gt;        .route(&amp;#34;/&amp;#34;, axum::routing::get(endpoints::main_handler).post(endpoints::main_handler));&lt;br/&gt;&lt;br/&gt;    if config.grasp.enable {&lt;br/&gt;        tracing::info!(&amp;#34;Git server is running&amp;#34;);&lt;br/&gt;        app = app.merge(git_server::router(&amp;amp;config));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    app = app&lt;br/&gt;        // enable cross-origin access&lt;br/&gt;        .route(&lt;br/&gt;            &amp;#34;/&amp;#34;,&lt;br/&gt;            axum::routing::options(|| async { hyper::StatusCode::NO_CONTENT }),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            cors::CorsLayer::new()&lt;br/&gt;                .allow_origin(cors::Any)&lt;br/&gt;                .allow_methods([Method::GET, Method::POST])&lt;br/&gt;                .allow_headers([header::CONTENT_TYPE]),&lt;br/&gt;        )&lt;br/&gt;        .layer(&lt;br/&gt;            TraceLayer::new_for_http()&lt;br/&gt;                .make_span_with(DefaultMakeSpan::default().include_headers(true)),&lt;br/&gt;        )&lt;br/&gt;        .layer(RequestDecompressionLayer::new())&lt;br/&gt;        .layer(Extension(Arc::new(RouterState::new(&lt;br/&gt;            config, n34_relay, relay_db,&lt;br/&gt;        ))));&lt;br/&gt;&lt;br/&gt;    axum::serve(&lt;br/&gt;        tokio::net::TcpListener::bind(addr).await?,&lt;br/&gt;        app.into_make_service_with_connect_info::&amp;lt;SocketAddr&amp;gt;(),&lt;br/&gt;    )&lt;br/&gt;    .with_graceful_shutdown(shutdown_signal())&lt;br/&gt;    .await?;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// #[tokio::main]&lt;br/&gt;// async fn main() -&amp;gt; ExitCode {&lt;br/&gt;//     if let Err(err) = try_main().await {&lt;br/&gt;//         eprintln!(&amp;#34;{err}&amp;#34;);&lt;br/&gt;//         return ExitCode::FAILURE;&lt;br/&gt;//     }&lt;br/&gt;// &lt;br/&gt;//     tracing::info!(&amp;#34;Exited gracefully without any errors&amp;#34;);&lt;br/&gt;//     ExitCode::SUCCESS&lt;br/&gt;// }&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:08Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs26f6s2h24gv8hucyhn9psr0qhny9axhwlppc046wk3ulw6857lxczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvaznt0m</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs26f6s2h24gv8hucyhn9psr0qhny9axhwlppc046wk3ulw6857lxczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvaznt0m" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{borrow::Cow, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::http::HeaderValue;&lt;br/&gt;use hyper::{&lt;br/&gt;    HeaderMap,&lt;br/&gt;    header::{self, AsHeaderName},&lt;br/&gt;};&lt;br/&gt;use nostr::message::MachineReadablePrefix;&lt;br/&gt;use nostr_relay_builder::builder::WritePolicyResult;&lt;br/&gt;use parking_lot::RwLock;&lt;br/&gt;&lt;br/&gt;/// Extension trait for managing a shared list of things&lt;br/&gt;#[easy_ext::ext(RwlockVecExt)]&lt;br/&gt;pub impl&amp;lt;T: PartialEq&amp;gt; Arc&amp;lt;RwLock&amp;lt;Vec&amp;lt;T&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;    /// Construct a new instance&lt;br/&gt;    fn new_empty() -&amp;gt; Self {&lt;br/&gt;        Arc::new(RwLock::new(Vec::new()))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the list is empty&lt;br/&gt;    fn is_empty(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.read().is_empty()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the given value exists in the list&lt;br/&gt;    fn contains(&amp;amp;self, val: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;        self.read().contains(val)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `true` if the giveing value is the first value in the list&lt;br/&gt;    fn is_first(&amp;amp;self, value: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;        self.read()&lt;br/&gt;            .first()&lt;br/&gt;            .is_some_and(|first_value| first_value == value)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extension trait for `RwLock` options&lt;br/&gt;#[easy_ext::ext(RwlockOption)]&lt;br/&gt;pub impl&amp;lt;T&amp;gt; RwLock&amp;lt;Option&amp;lt;T&amp;gt;&amp;gt; {&lt;br/&gt;    /// Returns true if the option is none&lt;br/&gt;    fn is_none(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.read().is_none()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extension trait for header map&lt;br/&gt;#[easy_ext::ext(HeaderMapExt)]&lt;br/&gt;pub impl &amp;amp;HeaderMap&amp;lt;HeaderValue&amp;gt; {&lt;br/&gt;    const NOSTR_JSON_MIME: &amp;amp;&amp;#39;static str = &amp;#34;application/nostr&#43;json&amp;#34;;&lt;br/&gt;    const NOSTR_JSON_RPC_MIME: &amp;amp;&amp;#39;static str = &amp;#34;application/nostr&#43;json&#43;rpc&amp;#34;;&lt;br/&gt;    const UPGRADE_MIME: &amp;amp;&amp;#39;static str = &amp;#34;upgrade&amp;#34;;&lt;br/&gt;    const WEBSOCKET_MIME: &amp;amp;&amp;#39;static str = &amp;#34;websocket&amp;#34;;&lt;br/&gt;&lt;br/&gt;    /// Checks if the header map contains the specified header and if its value&lt;br/&gt;    /// matches the given value.&lt;br/&gt;    #[inline]&lt;br/&gt;    fn is_contains(&amp;amp;self, header_name: impl AsHeaderName, header_value: &amp;amp;str) -&amp;gt; bool {&lt;br/&gt;        self.get(header_name)&lt;br/&gt;            .and_then(|content_value| content_value.to_str().ok())&lt;br/&gt;            .is_some_and(|content_str| content_str.eq_ignore_ascii_case(header_value))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the provided headers indicate an upgrade to a WebSocket&lt;br/&gt;    /// connection.&lt;br/&gt;    fn is_ws_upgrade(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.is_contains(header::CONNECTION, Self::UPGRADE_MIME)&lt;br/&gt;            &amp;amp;&amp;amp; self.is_contains(header::UPGRADE, Self::WEBSOCKET_MIME)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the provided headers indicate a NIP-86 request&lt;br/&gt;    fn is_nip86_req(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.is_contains(header::CONTENT_TYPE, Self::NOSTR_JSON_RPC_MIME)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the provided headers indicate a NIP-11 request&lt;br/&gt;    fn is_nip11_req(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.is_contains(header::ACCEPT, Self::NOSTR_JSON_MIME)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extension trait for [WritePolicyResult]&lt;br/&gt;#[easy_ext::ext(WritePolicyResultExt)]&lt;br/&gt;pub impl WritePolicyResult {&lt;br/&gt;    /// Reject result with `Blocked` prefix&lt;br/&gt;    #[inline]&lt;br/&gt;    fn blocked_reject&amp;lt;S&amp;gt;(msg: S) -&amp;gt; Self&lt;br/&gt;    where&lt;br/&gt;        S: Into&amp;lt;Cow&amp;lt;&amp;#39;static, str&amp;gt;&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        WritePolicyResult::reject(MachineReadablePrefix::Blocked, msg)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:22:04Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqstmycflcle4500s3txuxd7kfa5lsscdp5k5xcdqzjmh7gujgcfcqqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3fkpk9</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqstmycflcle4500s3txuxd7kfa5lsscdp5k5xcdqzjmh7gujgcfcqqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3fkpk9" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{collections::HashMap, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::{body::Body, response::Response};&lt;br/&gt;use hyper::{HeaderMap, StatusCode};&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, Kind},&lt;br/&gt;    filter::Filter,&lt;br/&gt;};&lt;br/&gt;use nostr_database::NostrDatabase;&lt;br/&gt;&lt;br/&gt;use crate::git_server::{PublicKeyAndRepoPath, ref_update_pkt_line::RefUpdatePkt};&lt;br/&gt;&lt;br/&gt;/// unpack-status = PKT-LINE(&amp;#34;unpack&amp;#34; SP unpack-result)&lt;br/&gt;/// unpack-result = &amp;#34;ok&amp;#34; / error-msg&lt;br/&gt;const UNPACK_OK: &amp;amp;[u8] = b&amp;#34;unpack ok&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Checks if the provided headers contain the `Git-Protocol: version=2` header.&lt;br/&gt;pub fn contains_git_v2(headers: &amp;amp;HeaderMap) -&amp;gt; bool {&lt;br/&gt;    headers&lt;br/&gt;        .get(&amp;#34;Git-Protocol&amp;#34;)&lt;br/&gt;        .is_some_and(|value| value == &amp;#34;version=2&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Add the data length before it. `{length}{data}`&lt;br/&gt;fn pkt_line(data: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let mut result = format!(&amp;#34;{:04x}&amp;#34;, data.len() &#43; 4).into_bytes();&lt;br/&gt;    result.extend_from_slice(data);&lt;br/&gt;    result&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Make a pkt_line with a side-band. `{length}{channel}pkt_line({data})`&lt;br/&gt;fn sideband_pkt_line(channel: u8, data: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let pktline = if data == b&amp;#34;0000&amp;#34; {&lt;br/&gt;        data&lt;br/&gt;    } else {&lt;br/&gt;        &amp;amp;pkt_line(data)&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    // 4 bytes for length &#43; 1 byte for channel &#43; pktline&lt;br/&gt;    let mut result = format!(&amp;#34;{:04x}&amp;#34;, pktline.len() &#43; 5).into_bytes();&lt;br/&gt;    result.push(channel);&lt;br/&gt;    result.extend_from_slice(pktline);&lt;br/&gt;    result&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Formats an error message using Git&amp;#39;s pkt-line report-status.&lt;br/&gt;fn git_errors_response(&lt;br/&gt;    refs_errors: &amp;amp;HashMap&amp;lt;&amp;amp;str, &amp;amp;&amp;#39;static str&amp;gt;,&lt;br/&gt;    all_refs: &amp;amp;[RefUpdatePkt],&lt;br/&gt;    capabilities: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Vec&amp;lt;u8&amp;gt; {&lt;br/&gt;    let mut response = Vec::new();&lt;br/&gt;    let caps_contains = |cap| capabilities.split(&amp;#34; &amp;#34;).any(|c| c == cap);&lt;br/&gt;    let use_sideband = caps_contains(&amp;#34;side-band-64k&amp;#34;) || caps_contains(&amp;#34;side-band&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // report-status = unpack-status&lt;br/&gt;    //                 1*(command-status)&lt;br/&gt;    //                 flush-pkt&lt;br/&gt;    if caps_contains(&amp;#34;report-status&amp;#34;) || caps_contains(&amp;#34;report-status-v2&amp;#34;) {&lt;br/&gt;        // command-status = command-ok / command-fail&lt;br/&gt;        // command-ok     = PKT-LINE(&amp;#34;ok&amp;#34; SP refname)&lt;br/&gt;        // command-fail   = PKT-LINE(&amp;#34;ng&amp;#34; SP refname SP error-msg)&lt;br/&gt;        let ng_line = |ref_name, msg| format!(&amp;#34;ng {ref_name} {msg}&amp;#34;);&lt;br/&gt;        let ok_line = |ref_name| format!(&amp;#34;ok {ref_name}&amp;#34;);&lt;br/&gt;&lt;br/&gt;        if use_sideband {&lt;br/&gt;            response.extend_from_slice(&amp;amp;sideband_pkt_line(1, UNPACK_OK));&lt;br/&gt;            for ref_update in all_refs {&lt;br/&gt;                if let Some(err) = refs_errors.get(ref_update.ref_name) {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;sideband_pkt_line(&lt;br/&gt;                        1,&lt;br/&gt;                        ng_line(&amp;amp;ref_update.ref_name, err).as_bytes(),&lt;br/&gt;                    ));&lt;br/&gt;                } else {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;sideband_pkt_line(&lt;br/&gt;                        1,&lt;br/&gt;                        ok_line(ref_update.ref_name).as_bytes(),&lt;br/&gt;                    ));&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            // side-band flush-pkt&lt;br/&gt;            response.extend_from_slice(&amp;amp;sideband_pkt_line(1, b&amp;#34;0000&amp;#34;));&lt;br/&gt;            // flush-pkt&lt;br/&gt;            response.extend_from_slice(b&amp;#34;0000&amp;#34;);&lt;br/&gt;        } else {&lt;br/&gt;            response.extend_from_slice(&amp;amp;pkt_line(UNPACK_OK));&lt;br/&gt;            for ref_update in all_refs {&lt;br/&gt;                if let Some(err) = refs_errors.get(ref_update.ref_name) {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;pkt_line(&lt;br/&gt;                        ng_line(&amp;amp;ref_update.ref_name, err).as_bytes(),&lt;br/&gt;                    ));&lt;br/&gt;                } else {&lt;br/&gt;                    response.extend_from_slice(&amp;amp;pkt_line(ok_line(ref_update.ref_name).as_bytes()));&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            // flush-pkt&lt;br/&gt;            response.extend_from_slice(b&amp;#34;0000&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;    response&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates an error response that Git clients will display to users.&lt;br/&gt;pub fn git_receive_pack_error(&lt;br/&gt;    refs_errors: &amp;amp;HashMap&amp;lt;&amp;amp;str, &amp;amp;&amp;#39;static str&amp;gt;,&lt;br/&gt;    all_refs: &amp;amp;[RefUpdatePkt],&lt;br/&gt;    capabilities: &amp;amp;str,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let body = git_errors_response(refs_errors, all_refs, capabilities);&lt;br/&gt;&lt;br/&gt;    Response::builder()&lt;br/&gt;        .status(StatusCode::OK)&lt;br/&gt;        .header(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/x-git-receive-pack-result&amp;#34;)&lt;br/&gt;        .body(Body::from(body))&lt;br/&gt;        .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extract the refs from the repository state event&lt;br/&gt;#[inline]&lt;br/&gt;pub fn extract_refs(repo_state: &amp;amp;Event) -&amp;gt; impl Iterator&amp;lt;Item = (&amp;amp;str, &amp;amp;str)&amp;gt; {&lt;br/&gt;    repo_state.tags.iter().filter_map(|tag| {&lt;br/&gt;        let tag_kind = tag.as_slice().first()?.as_str();&lt;br/&gt;        if (tag_kind.starts_with(&amp;#34;refs/heads/&amp;#34;) || tag_kind.starts_with(&amp;#34;refs/tags/&amp;#34;))&lt;br/&gt;            &amp;amp;&amp;amp; let Some(commit_id) = tag.content()&lt;br/&gt;        {&lt;br/&gt;            return Some((tag_kind, commit_id));&lt;br/&gt;        }&lt;br/&gt;        None&lt;br/&gt;    })&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Check if the ref match with the repository state&lt;br/&gt;pub fn check_ref_update(ref_update: &amp;amp;RefUpdatePkt, repo_state: &amp;amp;Event) -&amp;gt; Result&amp;lt;(), &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    let ref_commit = extract_refs(repo_state)&lt;br/&gt;        .find_map(|(name, commit)| (ref_update.ref_name == name).then_some(commit));&lt;br/&gt;&lt;br/&gt;    // Return an error if the ref is not found and the operation is not a delete&lt;br/&gt;    if ref_commit.is_none() &amp;amp;&amp;amp; !ref_update.is_delete() {&lt;br/&gt;        return Err(&lt;br/&gt;            &amp;#34;Reference not found. The reference must exist in the repository state to update it&amp;#34;,&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Return an error if attempting to delete a ref that is still present in the&lt;br/&gt;    // repository state&lt;br/&gt;    if ref_commit.is_some() &amp;amp;&amp;amp; ref_update.is_delete() {&lt;br/&gt;        return Err(&amp;#34;Cannot delete reference: it still exists in the repository state&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Return an error if the ref&amp;#39;s current commit doesn&amp;#39;t match the new commit&lt;br/&gt;    if let Some(commit) = ref_commit&lt;br/&gt;        &amp;amp;&amp;amp; commit != ref_update.new_commit&lt;br/&gt;    {&lt;br/&gt;        return Err(&lt;br/&gt;            &amp;#34;Commit mismatch: the new commit doesn&amp;#39;t match the repository state for this reference&amp;#34;,&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // Accept if:&lt;br/&gt;    // - ref is not found the the commit is delete&lt;br/&gt;    // - ref is found and match the new commit&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Check if the push should be continue, if not return the error message&lt;br/&gt;pub async fn is_legal_push&amp;lt;&amp;#39;a&amp;gt;(&lt;br/&gt;    ref_updates: &amp;amp;[RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt;],&lt;br/&gt;    db: &amp;amp;Arc&amp;lt;dyn NostrDatabase&amp;gt;,&lt;br/&gt;    repo: &amp;amp;PublicKeyAndRepoPath,&lt;br/&gt;) -&amp;gt; Result&amp;lt;HashMap&amp;lt;&amp;amp;&amp;#39;a str, &amp;amp;&amp;#39;static str&amp;gt;, (StatusCode, &amp;amp;&amp;#39;static str)&amp;gt; {&lt;br/&gt;    // Repo announcement from the author&lt;br/&gt;    let Some(repo_announcement) = db&lt;br/&gt;        .query(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .author(repo.public_key)&lt;br/&gt;                .identifier(&amp;amp;repo.repo_name)&lt;br/&gt;                .kind(Kind::GitRepoAnnouncement),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .map_err(|_| {&lt;br/&gt;            (&lt;br/&gt;                StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;                &amp;#34;Database error. Try to push later :(&amp;#34;,&lt;br/&gt;            )&lt;br/&gt;        })?&lt;br/&gt;        .first_owned()&lt;br/&gt;    else {&lt;br/&gt;        return Err((&lt;br/&gt;            StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;            &amp;#34;No repository announcement found. It should be there but it&amp;#39;s not. Broadcast it \&lt;br/&gt;             please :)&amp;#34;,&lt;br/&gt;        ));&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    // Repo state event from the author or one of the maintainers&lt;br/&gt;    let Some(repo_state) = db&lt;br/&gt;        .query(&lt;br/&gt;            Filter::new()&lt;br/&gt;                .author(repo.public_key)&lt;br/&gt;                .authors(crate::utils::get_maintainers(&amp;amp;repo_announcement).copied())&lt;br/&gt;                .identifier(&amp;amp;repo.repo_name)&lt;br/&gt;                .kind(Kind::RepoState),&lt;br/&gt;        )&lt;br/&gt;        .await&lt;br/&gt;        .map_err(|_| {&lt;br/&gt;            (&lt;br/&gt;                StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;                &amp;#34;Database error. Try to push later :(&amp;#34;,&lt;br/&gt;            )&lt;br/&gt;        })?&lt;br/&gt;        .first_owned()&lt;br/&gt;    else {&lt;br/&gt;        return Err((&lt;br/&gt;            StatusCode::BAD_REQUEST,&lt;br/&gt;            &amp;#34;No repository state announcements found. Broadcast it to the relay first&amp;#34;,&lt;br/&gt;        ));&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    Ok(ref_updates&lt;br/&gt;        .iter()&lt;br/&gt;        .filter_map(|ref_update| {&lt;br/&gt;            check_ref_update(ref_update, &amp;amp;repo_state)&lt;br/&gt;                .err()&lt;br/&gt;                .map(|err| (ref_update.ref_name, err))&lt;br/&gt;        })&lt;br/&gt;        .collect())&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:57Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs9dl3gnlva7vqgg0nyh556klf04d7ndejkdcntvze8p6g5x9gp53gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8vdwtv</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs9dl3gnlva7vqgg0nyh556klf04d7ndejkdcntvze8p6g5x9gp53gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv8vdwtv" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::PathBuf;&lt;br/&gt;&lt;br/&gt;use nostr_database::DatabaseError;&lt;br/&gt;&lt;br/&gt;use crate::relay::GrpcError;&lt;br/&gt;use crate::relay::RhaiPluginsError;&lt;br/&gt;&lt;br/&gt;/// Relay `Result` type&lt;br/&gt;pub type RelayResult&amp;lt;T&amp;gt; = Result&amp;lt;T, RelayError&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// Relay errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum RelayError {&lt;br/&gt;    #[error(&amp;#34;IO Error: {0}&amp;#34;)]&lt;br/&gt;    Io(#[from] std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Database error: {0}&amp;#34;)]&lt;br/&gt;    Database(#[from] DatabaseError),&lt;br/&gt;    #[error(&amp;#34;gRPC initialization error: {0}&amp;#34;)]&lt;br/&gt;    Grpc(#[from] GrpcError),&lt;br/&gt;    #[error(&amp;#34;Rhai error: {0}&amp;#34;)]&lt;br/&gt;    Rhai(#[from] RhaiPluginsError),&lt;br/&gt;    #[error(&amp;#34;File system Error: path `{0}` {1}&amp;#34;)]&lt;br/&gt;    Fs(PathBuf, String),&lt;br/&gt;    #[error(&amp;#34;Config error: {0}&amp;#34;)]&lt;br/&gt;    Config(String),&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:53Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsqh4237jz76k9tpu355972kn0797xhpqhxgvcw8s6r2sjunprtz3gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvxg7dey</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsqh4237jz76k9tpu355972kn0797xhpqhxgvcw8s6r2sjunprtz3gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvxg7dey" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    body::Bytes,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;&lt;br/&gt;use super::git_command::GitCommand;&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;/// Handles a git-upload-pack request for a repository.&lt;br/&gt;/// Verifies the repository exists and processes the received pack data.&lt;br/&gt;/// Returns a successful response with the pack data or an appropriate error.&lt;br/&gt;pub async fn upload_pack(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    headers: hyper::HeaderMap,&lt;br/&gt;    body: Bytes,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    match GitCommand::new(&amp;amp;state.config.grasp.git_path, &amp;amp;repo_path)&lt;br/&gt;        .upload_pack(&amp;amp;body, super::utils::contains_git_v2(&amp;amp;headers))&lt;br/&gt;        .await&lt;br/&gt;    {&lt;br/&gt;        Ok(response_body) =&amp;gt; {&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::OK)&lt;br/&gt;                .header(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/x-git-upload-pack-result&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Connection&amp;#34;, &amp;#34;Keep-Alive&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Transfer-Encoding&amp;#34;, &amp;#34;chunked&amp;#34;)&lt;br/&gt;                .header(&amp;#34;X-Content-Type-Options&amp;#34;, &amp;#34;nosniff&amp;#34;)&lt;br/&gt;                .body(response_body)&lt;br/&gt;                .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;        Err(err_msg) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, err_msg).into_response(),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:46Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsqguqdrrcuenq5zy2ln493up9cj9ukapefknqphde3yxmw9xzknpczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrey4y7</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsqguqdrrcuenq5zy2ln493up9cj9ukapefknqphde3yxmw9xzknpczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrey4y7" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::time::{SystemTime, UNIX_EPOCH};&lt;br/&gt;&lt;br/&gt;use axum::http::HeaderValue;&lt;br/&gt;use base64::{&lt;br/&gt;    Engine,&lt;br/&gt;    prelude::{BASE64_STANDARD, BASE64_STANDARD_NO_PAD, BASE64_URL_SAFE, BASE64_URL_SAFE_NO_PAD},&lt;br/&gt;};&lt;br/&gt;use hyper::{HeaderMap, Method};&lt;br/&gt;use nostr::{&lt;br/&gt;    event::{Event, Kind, Tag, TagKind},&lt;br/&gt;    filter::Alphabet,&lt;br/&gt;    key::PublicKey,&lt;br/&gt;    util::JsonUtil,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    endpoints::nip86::errors::{ApiError, ApiResult},&lt;br/&gt;    ext_traits::RwlockVecExt,&lt;br/&gt;    router_state::RouterState,&lt;br/&gt;    utils as crate_utils,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;const NOSTR_SCHEME: &amp;amp;str = &amp;#34;Nostr &amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Extracts the authentication token from the headers, ensuring it matches the&lt;br/&gt;/// expected scheme.&lt;br/&gt;pub fn get_auth_token(headers: &amp;amp;HeaderMap&amp;lt;HeaderValue&amp;gt;) -&amp;gt; ApiResult&amp;lt;&amp;amp;str&amp;gt; {&lt;br/&gt;    headers&lt;br/&gt;        .get(&amp;#34;authorization&amp;#34;)&lt;br/&gt;        .ok_or(ApiError::NoAuthHeader)&lt;br/&gt;        .and_then(|header| header.to_str().map_err(|_| ApiError::InvalidAuthHeader))&lt;br/&gt;        .and_then(|auth_token| {&lt;br/&gt;            if let Some((&amp;#34;&amp;#34;, auth_token)) = auth_token.trim().split_once(NOSTR_SCHEME) {&lt;br/&gt;                Ok(auth_token)&lt;br/&gt;            } else {&lt;br/&gt;                Err(ApiError::NotNostrAuth)&lt;br/&gt;            }&lt;br/&gt;        })&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Verifies a NIP-98 HTTP auth token and returns its author and payload hash if&lt;br/&gt;/// valid.&lt;br/&gt;///&lt;br/&gt;/// The token must be from an admin, properly signed, and match the request&lt;br/&gt;/// method and relay URL.&lt;br/&gt;#[must_use = &amp;#34;payload hash must be checked&amp;#34;]&lt;br/&gt;pub fn get_payload_hash(&lt;br/&gt;    base64_str: impl AsRef&amp;lt;str&amp;gt;,&lt;br/&gt;    request_method: &amp;amp;Method,&lt;br/&gt;    state: &amp;amp;RouterState,&lt;br/&gt;) -&amp;gt; ApiResult&amp;lt;(PublicKey, String)&amp;gt; {&lt;br/&gt;    // Attempt to decode the base64 string using various base64 standards (standard,&lt;br/&gt;    // standard without padding, URL-safe, and URL-safe without padding) since&lt;br/&gt;    // NIP-98 (HTTP Auth) does not specify which base64 variant to use.&lt;br/&gt;    let event = Event::from_json(&lt;br/&gt;        BASE64_STANDARD&lt;br/&gt;            .decode(base64_str.as_ref())&lt;br/&gt;            .or_else(|_| BASE64_STANDARD_NO_PAD.decode(base64_str.as_ref()))&lt;br/&gt;            .or_else(|_| BASE64_URL_SAFE.decode(base64_str.as_ref()))&lt;br/&gt;            .or_else(|_| BASE64_URL_SAFE_NO_PAD.decode(base64_str.as_ref()))&lt;br/&gt;            .map_err(|_| ApiError::InvalidBase64Token)?,&lt;br/&gt;    )&lt;br/&gt;    .map_err(|_| ApiError::InvalidNostrEventToken)?;&lt;br/&gt;&lt;br/&gt;    if !state.config.relay.admins.contains(&amp;amp;event.pubkey) {&lt;br/&gt;        return Err(ApiError::NotAdmin);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if !event.verify_id() {&lt;br/&gt;        return Err(ApiError::InvalidEventId);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if event.kind != Kind::HttpAuth {&lt;br/&gt;        return Err(ApiError::NotHttpAuthKind);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if !event.content.is_empty() {&lt;br/&gt;        return Err(ApiError::NonEmptyHttpAuthContent);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if let Ok(current_time) = SystemTime::now().duration_since(UNIX_EPOCH)&lt;br/&gt;        &amp;amp;&amp;amp; (current_time.as_secs() - 100) &amp;gt; event.created_at.as_secs()&lt;br/&gt;    {&lt;br/&gt;        return Err(ApiError::OldAuthEvent);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if crate_utils::remove_proto(&lt;br/&gt;        event&lt;br/&gt;            .tags&lt;br/&gt;            .find(TagKind::single_letter(Alphabet::U, false))&lt;br/&gt;            .and_then(Tag::content)&lt;br/&gt;            .ok_or(ApiError::MissingRlayUrl)?,&lt;br/&gt;    ) != state.config.relay.domain.as_str()&lt;br/&gt;    {&lt;br/&gt;        return Err(ApiError::IncorrectRelayUrl);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if event&lt;br/&gt;        .tags&lt;br/&gt;        .find(TagKind::Method)&lt;br/&gt;        .and_then(Tag::content)&lt;br/&gt;        .ok_or(ApiError::MissingMethod)?&lt;br/&gt;        != request_method.as_str()&lt;br/&gt;    {&lt;br/&gt;        return Err(ApiError::IncorrectRequestMethod);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if !event.verify_signature() {&lt;br/&gt;        return Err(ApiError::InvalidSignature);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok((&lt;br/&gt;        event.pubkey,&lt;br/&gt;        event&lt;br/&gt;            .tags&lt;br/&gt;            .find(TagKind::Payload)&lt;br/&gt;            .and_then(Tag::content)&lt;br/&gt;            .ok_or(ApiError::MissingPayloadHash)?&lt;br/&gt;            .to_owned(),&lt;br/&gt;    ))&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Removes the line number and column details from a serde_json error message,&lt;br/&gt;/// if present.&lt;br/&gt;pub fn remove_line_number(err: &amp;amp;str) -&amp;gt; &amp;amp;str {&lt;br/&gt;    err.rsplit_once(&amp;#34; at line &amp;#34;).map_or(err, |(msg, _)| msg)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:42Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs03s7qe32kl4a6thwq645e0zufwfw34ttvgrmf3nqv85d4wg84xegzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv69uhr9</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs03s7qe32kl4a6thwq645e0zufwfw34ttvgrmf3nqv85d4wg84xegzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv69uhr9" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use crate::utils;&lt;br/&gt;&lt;br/&gt;/// The null/zero SHA-1 hash indicating deletion or non-existence.&lt;br/&gt;const NULL_OID: &amp;amp;str = &amp;#34;0000000000000000000000000000000000000000&amp;#34;;&lt;br/&gt;&lt;br/&gt;/// Represents a ref update pkt-line in a Git protocol.&lt;br/&gt;#[derive(Debug)]&lt;br/&gt;pub struct RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// The old commit hash (before the update).&lt;br/&gt;    pub old_commit:   &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// The new commit hash (after the update).&lt;br/&gt;    pub new_commit:   &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// The name of the reference (e.g., &amp;#34;refs/heads/master&amp;#34;).&lt;br/&gt;    pub ref_name:     &amp;amp;&amp;#39;a str,&lt;br/&gt;    /// The capabilities.&lt;br/&gt;    pub capabilities: Option&amp;lt;&amp;amp;&amp;#39;a str&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;a&amp;gt; RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// Parses the ref update payload (without the 4-byte length prefix).&lt;br/&gt;    ///&lt;br/&gt;    /// Expected format: `&amp;lt;old-oid&amp;gt; &amp;lt;new-oid&amp;gt; &amp;lt;ref-name&amp;gt;[\0&amp;lt;capabilities&amp;gt;]`&lt;br/&gt;    pub fn parse(payload: &amp;amp;&amp;#39;a [u8]) -&amp;gt; Result&amp;lt;Self, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let payload = payload.strip_suffix(b&amp;#34;\n&amp;#34;).unwrap_or(payload);&lt;br/&gt;&lt;br/&gt;        let str_payload =&lt;br/&gt;            str::from_utf8(payload).map_err(|_| &amp;#34;Invalid pkt-line content: not valid UTF-8&amp;#34;)?;&lt;br/&gt;        let mut parts = str_payload.splitn(3, &amp;#39; &amp;#39;);&lt;br/&gt;&lt;br/&gt;        let old_commit = parts.next().ok_or(&amp;#34;Missing old commit hash&amp;#34;)?;&lt;br/&gt;        let new_commit = parts.next().ok_or(&amp;#34;Missing new commit hash&amp;#34;)?;&lt;br/&gt;        let ref_with_caps = parts.next().ok_or(&amp;#34;Missing ref name&amp;#34;)?;&lt;br/&gt;&lt;br/&gt;        // pack-protocol: PKT-LINE(command NUL capability-list)&lt;br/&gt;        let (ref_name, capabilities) = match ref_with_caps.split_once(&amp;#39;\0&amp;#39;) {&lt;br/&gt;            Some((name, caps)) =&amp;gt; (name, Some(caps.trim())),&lt;br/&gt;            None =&amp;gt; (ref_with_caps, None),&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        // Validate OIDs&lt;br/&gt;        if !utils::is_valid_sha1(old_commit) {&lt;br/&gt;            return Err(&amp;#34;Invalid old commit hash&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        if !utils::is_valid_sha1(new_commit) {&lt;br/&gt;            return Err(&amp;#34;Invalid new commit hash&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // Validate ref name&lt;br/&gt;        if !ref_name.starts_with(&amp;#34;refs/&amp;#34;) {&lt;br/&gt;            return Err(&amp;#34;Invalid ref name: must start with &amp;#39;refs/&amp;#39;&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Ok(Self {&lt;br/&gt;            old_commit,&lt;br/&gt;            new_commit,&lt;br/&gt;            ref_name,&lt;br/&gt;            capabilities,&lt;br/&gt;        })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `true` if this is a branch/ref deletion (new commit is null&lt;br/&gt;    /// OID).&lt;br/&gt;    #[inline]&lt;br/&gt;    #[must_use]&lt;br/&gt;    pub fn is_delete(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.new_commit == NULL_OID&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `true` if this is a new branch/ref creation (old commit is null&lt;br/&gt;    /// OID).&lt;br/&gt;    #[allow(dead_code)] // Used in unit tests&lt;br/&gt;    pub fn is_create(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.old_commit == NULL_OID&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses the 4-byte hex length prefix of a pkt-line.&lt;br/&gt;/// Returns `None` for special packets (flush=0000, delim=0001, etc.)&lt;br/&gt;fn parse_pkt_length(data: &amp;amp;[u8]) -&amp;gt; Result&amp;lt;Option&amp;lt;usize&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    if data.len() &amp;lt; 4 {&lt;br/&gt;        return Err(&amp;#34;Invalid pkt-line: input too short&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let len_str =&lt;br/&gt;        str::from_utf8(&amp;amp;data[..4]).map_err(|_| &amp;#34;Invalid pkt-line length: not valid UTF-8&amp;#34;)?;&lt;br/&gt;&lt;br/&gt;    let len = u16::from_str_radix(len_str, 16)&lt;br/&gt;        .map_err(|_| &amp;#34;Invalid pkt-line length: not valid hex&amp;#34;)? as usize;&lt;br/&gt;&lt;br/&gt;    match len {&lt;br/&gt;        0 =&amp;gt; Ok(None), // flush packet (0000)&lt;br/&gt;        1 =&amp;gt; Ok(None), // delimiter packet (0001) - skip&lt;br/&gt;        2 =&amp;gt; Ok(None), // response-end packet (0002) - skip&lt;br/&gt;        3 =&amp;gt; Err(&amp;#34;Invalid pkt-line: length too short&amp;#34;),&lt;br/&gt;        _ =&amp;gt; Ok(Some(len)),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;fn collect_pkt_lines(body: &amp;amp;[u8]) -&amp;gt; Result&amp;lt;Vec&amp;lt;&amp;amp;[u8]&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    let mut pkts = Vec::new();&lt;br/&gt;    let mut pos = 0;&lt;br/&gt;&lt;br/&gt;    while pos &amp;lt; body.len() {&lt;br/&gt;        // Need at least 4 bytes for the length prefix&lt;br/&gt;        if body.len() - pos &amp;lt; 4 {&lt;br/&gt;            break;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // Parse length prefix&lt;br/&gt;        let Some(pkt_len) = parse_pkt_length(&amp;amp;body[pos..])? else {&lt;br/&gt;            break;&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        // Validate we have enough data&lt;br/&gt;        if pos &#43; pkt_len &amp;gt; body.len() {&lt;br/&gt;            return Err(&amp;#34;Invalid pkt-line: declared length exceeds data length&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        // push the pkt (skip the 4-byte length prefix)&lt;br/&gt;        let pkt = &amp;amp;body[pos &#43; 4..pos &#43; pkt_len];&lt;br/&gt;        pkts.push(pkt);&lt;br/&gt;&lt;br/&gt;        pos &#43;= pkt_len;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(pkts)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Parses pkt-line formatted ref-updates from a byte slice.&lt;br/&gt;///&lt;br/&gt;/// Stops parsing when encountering a flush packet (0000) or end of data.&lt;br/&gt;/// Returns the parsed ref updates&lt;br/&gt;pub fn parse_pkt_lines&amp;lt;&amp;#39;a&amp;gt;(body: &amp;amp;&amp;#39;a [u8]) -&amp;gt; Result&amp;lt;Vec&amp;lt;RefUpdatePkt&amp;lt;&amp;#39;a&amp;gt;&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;    let mut ref_updates = Vec::new();&lt;br/&gt;&lt;br/&gt;    // Parse only pkts that looks like command, where the command is:&lt;br/&gt;    //  command =  create / delete / update&lt;br/&gt;    //  create  =  zero-id SP new-id  SP name&lt;br/&gt;    //  delete  =  old-id  SP zero-id SP name&lt;br/&gt;    //  update  =  old-id  SP new-id  SP name&lt;br/&gt;    //&lt;br/&gt;    //  old-id  =  obj-id&lt;br/&gt;    //  new-id  =  obj-id&lt;br/&gt;    //&lt;br/&gt;    // This because we may receive commands and may receive `push-cert` that&lt;br/&gt;    // contains commands `*PKT-LINE(command LF)`. So this parsers will works with&lt;br/&gt;    // both.&lt;br/&gt;    for pkt in collect_pkt_lines(body)? {&lt;br/&gt;        let Ok(pkt_str) = str::from_utf8(pkt) else {&lt;br/&gt;            continue;&lt;br/&gt;        };&lt;br/&gt;        let mut parts = pkt_str.split(&amp;#39; &amp;#39;);&lt;br/&gt;&lt;br/&gt;        if utils::is_valid_sha1(parts.next().unwrap_or_default())&lt;br/&gt;            &amp;amp;&amp;amp; utils::is_valid_sha1(parts.next().unwrap_or_default())&lt;br/&gt;        {&lt;br/&gt;            // it looks like a command&lt;br/&gt;            ref_updates.push(RefUpdatePkt::parse(pkt)?);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(ref_updates)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(test)]&lt;br/&gt;mod tests {&lt;br/&gt;    use super::*;&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn parse_single_ref_update() {&lt;br/&gt;        let data = b&amp;#34;006bac281124fd463f368106445a4fe4eb251d9c7d7a 4559b8048c334a7e61c76a622cf7cd578a6af406 refs/heads/test2-file&amp;#34;;&lt;br/&gt;&lt;br/&gt;        let updates = parse_pkt_lines(data).unwrap();&lt;br/&gt;        assert_eq!(updates.len(), 1);&lt;br/&gt;        assert_eq!(updates[0].ref_name, &amp;#34;refs/heads/test2-file&amp;#34;);&lt;br/&gt;        assert_eq!(&lt;br/&gt;            updates[0].old_commit,&lt;br/&gt;            &amp;#34;ac281124fd463f368106445a4fe4eb251d9c7d7a&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(&lt;br/&gt;            updates[0].new_commit,&lt;br/&gt;            &amp;#34;4559b8048c334a7e61c76a622cf7cd578a6af406&amp;#34;&lt;br/&gt;        );&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn parse_create_branch() {&lt;br/&gt;        let payload = b&amp;#34;0000000000000000000000000000000000000000 53e284c5c3e8b8310077a43d09fd391456f582df refs/heads/new-branch&amp;#34;;&lt;br/&gt;        let update = RefUpdatePkt::parse(payload).unwrap();&lt;br/&gt;&lt;br/&gt;        assert!(update.is_create());&lt;br/&gt;        assert!(!update.is_delete());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn parse_delete_branch() {&lt;br/&gt;        let payload = b&amp;#34;53e284c5c3e8b8310077a43d09fd391456f582df 0000000000000000000000000000000000000000 refs/heads/old-branch&amp;#34;;&lt;br/&gt;        let update = RefUpdatePkt::parse(payload).unwrap();&lt;br/&gt;&lt;br/&gt;        assert!(update.is_delete());&lt;br/&gt;        assert!(!update.is_create());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn flush_stops_parsing() {&lt;br/&gt;        let mut data = Vec::new();&lt;br/&gt;        data.extend_from_slice(b&amp;#34;00b10000000000000000000000000000000000000000 ac281124fd463f368106445a4fe4eb251d9c7d7a refs/heads/master\0report-status-v2 side-band-64k object-format=sha1 agent=git/2.51.2-Linux\n&amp;#34;);&lt;br/&gt;        data.extend_from_slice(b&amp;#34;0000&amp;#34;); // Flush&lt;br/&gt;        data.extend_from_slice(b&amp;#34;PACK\x00\x00\x00\x02&amp;#34;); // PACK header&lt;br/&gt;&lt;br/&gt;        let updates = parse_pkt_lines(&amp;amp;data).unwrap();&lt;br/&gt;        assert_eq!(updates.len(), 1);&lt;br/&gt;&lt;br/&gt;        let update = updates.first().unwrap();&lt;br/&gt;        assert!(update.is_create());&lt;br/&gt;        assert_eq!(&lt;br/&gt;            update.new_commit,&lt;br/&gt;            &amp;#34;ac281124fd463f368106445a4fe4eb251d9c7d7a&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(update.ref_name, &amp;#34;refs/heads/master&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    #[test]&lt;br/&gt;    fn two_ref_updates() {&lt;br/&gt;        let mut data = Vec::new();&lt;br/&gt;        // first pkt-line&lt;br/&gt;        data.extend_from_slice(b&amp;#34;00B2ac281124fd463f368106445a4fe4eb251d9c7d7a 4559b8048c334a7e61c76a622cf7cd578a6af406 refs/heads/master\0 report-status-v2 side-band-64k object-format=sha1 agent=git/2.51.2-Linux\n&amp;#34;);&lt;br/&gt;        // second pkt-line&lt;br/&gt;        data.extend_from_slice(b&amp;#34;006b4559b8048c334a7e61c76a622cf7cd578a6af406 53e284c5c3e8b8310077a43d09fd391456f582df refs/heads/test2-file&amp;#34;);&lt;br/&gt;        data.extend_from_slice(b&amp;#34;0000&amp;#34;); // Flush&lt;br/&gt;        data.extend_from_slice(b&amp;#34;PACK\x00\x00\x00\x02&amp;#34;); // PACK header&lt;br/&gt;&lt;br/&gt;        let updates = parse_pkt_lines(&amp;amp;data).unwrap();&lt;br/&gt;        assert_eq!(updates.len(), 2);&lt;br/&gt;        let first_update = updates.first().unwrap();&lt;br/&gt;        let second_update = updates.last().unwrap();&lt;br/&gt;&lt;br/&gt;        assert_eq!(&lt;br/&gt;            first_update.old_commit,&lt;br/&gt;            &amp;#34;ac281124fd463f368106445a4fe4eb251d9c7d7a&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(&lt;br/&gt;            first_update.new_commit,&lt;br/&gt;            &amp;#34;4559b8048c334a7e61c76a622cf7cd578a6af406&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(first_update.ref_name, &amp;#34;refs/heads/master&amp;#34;);&lt;br/&gt;&lt;br/&gt;        assert_eq!(&lt;br/&gt;            second_update.old_commit,&lt;br/&gt;            &amp;#34;4559b8048c334a7e61c76a622cf7cd578a6af406&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(&lt;br/&gt;            second_update.new_commit,&lt;br/&gt;            &amp;#34;53e284c5c3e8b8310077a43d09fd391456f582df&amp;#34;&lt;br/&gt;        );&lt;br/&gt;        assert_eq!(second_update.ref_name, &amp;#34;refs/heads/test2-file&amp;#34;);&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:34Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs0qjlx4u0e02hst63mnzdfvsywz79s0yh4ekh5tmclgpktrjywtmszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv34et2d</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs0qjlx4u0e02hst63mnzdfvsywz79s0yh4ekh5tmclgpktrjywtmszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv34et2d" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Json,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;use nostr::{event::Kind, key::PublicKey};&lt;br/&gt;use serde::{Serialize, Serializer};&lt;br/&gt;&lt;br/&gt;use crate::endpoints::nip86::{errors::ApiError, params::PubKeyWithReason};&lt;br/&gt;&lt;br/&gt;/// API response body containing result and error information&lt;br/&gt;#[derive(Serialize)]&lt;br/&gt;pub struct Nip86Response {&lt;br/&gt;    /// Operation result&lt;br/&gt;    #[serde(skip_serializing_if = &amp;#34;Nip86Result::is_empty&amp;#34;)]&lt;br/&gt;    result: Nip86Result,&lt;br/&gt;    /// Error message if operation failed&lt;br/&gt;    #[serde(skip_serializing_if = &amp;#34;Option::is_none&amp;#34;)]&lt;br/&gt;    error:  Option&amp;lt;ApiError&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Possible results for a NIP86 API response&lt;br/&gt;#[derive(Serialize)]&lt;br/&gt;#[serde(untagged)]&lt;br/&gt;pub enum Nip86Result {&lt;br/&gt;    Empty,&lt;br/&gt;    #[serde(serialize_with = &amp;#34;always_true&amp;#34;)]&lt;br/&gt;    True,&lt;br/&gt;    PublicKeysAndReason(Vec&amp;lt;PubKeyWithReason&amp;gt;),&lt;br/&gt;    Kinds(Vec&amp;lt;Kind&amp;gt;),&lt;br/&gt;    #[serde(serialize_with = &amp;#34;hex_pubkeys_ser&amp;#34;)]&lt;br/&gt;    PublicKeys(Vec&amp;lt;PublicKey&amp;gt;),&lt;br/&gt;    SupportedMethods(&amp;amp;&amp;#39;static [&amp;amp;&amp;#39;static str]),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl Nip86Result {&lt;br/&gt;    /// Checks if the result is empty.&lt;br/&gt;    pub fn is_empty(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(self, Nip86Result::Empty)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl Nip86Response {&lt;br/&gt;    /// Creates a new successful response with the given result.&lt;br/&gt;    pub fn ok_res(result: Nip86Result) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            result,&lt;br/&gt;            error: None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Creates a new error response with the provided error.&lt;br/&gt;    pub fn err_res(err: ApiError) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            result: Nip86Result::Empty,&lt;br/&gt;            error:  Some(err),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl IntoResponse for Nip86Response {&lt;br/&gt;    fn into_response(self) -&amp;gt; Response {&lt;br/&gt;        (&lt;br/&gt;            self.error&lt;br/&gt;                .as_ref()&lt;br/&gt;                .map(|err| err.status_code())&lt;br/&gt;                .unwrap_or(StatusCode::OK),&lt;br/&gt;            Json(self),&lt;br/&gt;        )&lt;br/&gt;            .into_response()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl IntoResponse for Nip86Result {&lt;br/&gt;    fn into_response(self) -&amp;gt; Response {&lt;br/&gt;        Nip86Response::ok_res(self).into_response()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// function that returns true bool in serializeation&lt;br/&gt;fn always_true&amp;lt;S&amp;gt;(s: S) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Serializer,&lt;br/&gt;{&lt;br/&gt;    true.serialize(s)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Serialize to a list of hex public keys&lt;br/&gt;fn hex_pubkeys_ser&amp;lt;S&amp;gt;(pkeys: &amp;amp;[PublicKey], s: S) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;where&lt;br/&gt;    S: Serializer,&lt;br/&gt;{&lt;br/&gt;    pkeys&lt;br/&gt;        .iter()&lt;br/&gt;        .map(PublicKey::to_hex)&lt;br/&gt;        .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;        .serialize(s)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:22Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsdmmul85ae4apm05xqw8p76936w52aah0yal7229jsu4gm5jl8pqgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv02wzrn</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsdmmul85ae4apm05xqw8p76936w52aah0yal7229jsu4gm5jl8pqgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv02wzrn" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    body::Bytes,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;use nostr::nips::nip19::ToBech32;&lt;br/&gt;&lt;br/&gt;use super::{git_command::GitCommand, ref_update_pkt_line::parse_pkt_lines, utils};&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;/// Handles a git-receive-pack request for a repository.&lt;br/&gt;/// Verifies the repository exists and processes the received pack data.&lt;br/&gt;/// Returns a successful response with the pack data or an appropriate error.&lt;br/&gt;pub async fn receive_pack(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    body: Bytes,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let ref_updates = match parse_pkt_lines(&amp;amp;body) {&lt;br/&gt;        Ok(ref_updates) =&amp;gt; ref_updates,&lt;br/&gt;        Err(err) =&amp;gt; return (StatusCode::BAD_REQUEST, err).into_response(),&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    tracing::debug!(&lt;br/&gt;        &amp;#34;Ref updates: {ref_updates:?} of repo `{}/{}.git`&amp;#34;,&lt;br/&gt;        params.public_key.to_bech32().expect(&amp;#34;Infallible&amp;#34;),&lt;br/&gt;        params.repo_name&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    let capabilities = ref_updates&lt;br/&gt;        .iter()&lt;br/&gt;        .find_map(|ref_update| ref_update.capabilities)&lt;br/&gt;        .unwrap_or_default();&lt;br/&gt;&lt;br/&gt;    let refs_errors = match utils::is_legal_push(&amp;amp;ref_updates, &amp;amp;state.database, &amp;amp;params).await {&lt;br/&gt;        Ok(refs) =&amp;gt; refs,&lt;br/&gt;        Err((status_code, err)) =&amp;gt; {&lt;br/&gt;            tracing::error!(err = %err, &amp;#34;Failed to check the ref updates&amp;#34;);&lt;br/&gt;            return (status_code, err).into_response();&lt;br/&gt;        }&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    if !refs_errors.is_empty() {&lt;br/&gt;        return utils::git_receive_pack_error(&amp;amp;refs_errors, &amp;amp;ref_updates, capabilities);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    match GitCommand::new(&amp;amp;state.config.grasp.git_path, &amp;amp;repo_path)&lt;br/&gt;        .receive_pack(&amp;amp;body)&lt;br/&gt;        .await&lt;br/&gt;    {&lt;br/&gt;        Ok(response_body) =&amp;gt; {&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::OK)&lt;br/&gt;                .header(&amp;#34;Content-Type&amp;#34;, &amp;#34;application/x-git-receive-pack-result&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Connection&amp;#34;, &amp;#34;Keep-Alive&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Transfer-Encoding&amp;#34;, &amp;#34;chunked&amp;#34;)&lt;br/&gt;                .header(&amp;#34;X-Content-Type-Options&amp;#34;, &amp;#34;nosniff&amp;#34;)&lt;br/&gt;                .body(response_body)&lt;br/&gt;                .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;        Err(err_msg) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, err_msg).into_response(),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:22Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqszn5ejmsumdelkjk3ec44mm2keuf68c5c8uqxnylxdjn3p9hkqzgczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvxmyfm5</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqszn5ejmsumdelkjk3ec44mm2keuf68c5c8uqxnylxdjn3p9hkqzgczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvxmyfm5" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use parking_lot::RwLock;&lt;br/&gt;use serde::Deserialize;&lt;br/&gt;use strum::VariantNames;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    endpoints::nip86::{&lt;br/&gt;        errors::ApiResult,&lt;br/&gt;        params::{KindParam, PubKeyWithReason, PublicKeyParam, StringParam, UrlParam},&lt;br/&gt;        responses::Nip86Result,&lt;br/&gt;    },&lt;br/&gt;    router_state::RouterState,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;type EmptyParams = [u8; 0];&lt;br/&gt;&lt;br/&gt;/// The request body of the API&lt;br/&gt;#[derive(Deserialize, VariantNames)]&lt;br/&gt;#[serde(rename_all = &amp;#34;lowercase&amp;#34;, tag = &amp;#34;method&amp;#34;)]&lt;br/&gt;#[strum(serialize_all = &amp;#34;lowercase&amp;#34;)]&lt;br/&gt;#[allow(dead_code)] // `EmptyParams` will never read&lt;br/&gt;pub enum Nip86Request {&lt;br/&gt;    SupportedMethods { params: EmptyParams },&lt;br/&gt;    ChangeRelayName { params: StringParam },&lt;br/&gt;    ChangeRelayDescription { params: StringParam },&lt;br/&gt;    ChangeRelayIcon { params: UrlParam },&lt;br/&gt;    ChangeRelayBanner { params: UrlParam },&lt;br/&gt;    AllowPubkey { params: PubKeyWithReason },&lt;br/&gt;    AllowKind { params: KindParam },&lt;br/&gt;    AddAdmin { params: PublicKeyParam },&lt;br/&gt;    BanPubkey { params: PubKeyWithReason },&lt;br/&gt;    DisallowKind { params: KindParam },&lt;br/&gt;    RemoveAdmin { params: PublicKeyParam },&lt;br/&gt;    ListAllowedPubkeys { params: EmptyParams },&lt;br/&gt;    ListBannedPubkeys { params: EmptyParams },&lt;br/&gt;    ListAllowedKinds { params: EmptyParams },&lt;br/&gt;    ListDisallowedKinds { params: EmptyParams },&lt;br/&gt;    ListAdmins { params: EmptyParams },&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl Nip86Request {&lt;br/&gt;    /// Processes the request and returns the response.&lt;br/&gt;    pub async fn run(self, state: Arc&amp;lt;RouterState&amp;gt;) -&amp;gt; ApiResult&amp;lt;Nip86Result&amp;gt; {&lt;br/&gt;        match self {&lt;br/&gt;            Nip86Request::SupportedMethods { .. } =&amp;gt; {&lt;br/&gt;                Ok(Nip86Result::SupportedMethods(Self::VARIANTS))&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ChangeRelayName {&lt;br/&gt;                params: StringParam(new_name),&lt;br/&gt;            } =&amp;gt; {&lt;br/&gt;                *state.config.nip11.name.write() = Some(new_name);&lt;br/&gt;                Ok(Nip86Result::True)&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ChangeRelayDescription {&lt;br/&gt;                params: StringParam(new_description),&lt;br/&gt;            } =&amp;gt; {&lt;br/&gt;                *state.config.nip11.description.write() = Some(new_description);&lt;br/&gt;                Ok(Nip86Result::True)&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ChangeRelayIcon {&lt;br/&gt;                params: UrlParam(new_icon),&lt;br/&gt;            } =&amp;gt; {&lt;br/&gt;                *state.config.nip11.icon.write() = Some(new_icon);&lt;br/&gt;                Ok(Nip86Result::True)&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ChangeRelayBanner {&lt;br/&gt;                params: UrlParam(new_banner),&lt;br/&gt;            } =&amp;gt; {&lt;br/&gt;                *state.config.nip11.banner.write() = Some(new_banner);&lt;br/&gt;                Ok(Nip86Result::True)&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::AllowPubkey { params } =&amp;gt; {&lt;br/&gt;                list_value(&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.whitelist)),&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.blacklist)),&lt;br/&gt;                    params.pubkey,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::BanPubkey { params } =&amp;gt; {&lt;br/&gt;                list_value(&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.blacklist)),&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.whitelist)),&lt;br/&gt;                    params.pubkey,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::AllowKind {&lt;br/&gt;                params: KindParam(kind),&lt;br/&gt;            } =&amp;gt; {&lt;br/&gt;                list_value(&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.allowed_kinds)),&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.disallowed_kinds)),&lt;br/&gt;                    kind,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::DisallowKind {&lt;br/&gt;                params: KindParam(kind),&lt;br/&gt;            } =&amp;gt; {&lt;br/&gt;                list_value(&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.disallowed_kinds)),&lt;br/&gt;                    Some(Arc::clone(&amp;amp;state.config.relay.allowed_kinds)),&lt;br/&gt;                    kind,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::AddAdmin {&lt;br/&gt;                params: PublicKeyParam(pkey),&lt;br/&gt;            } =&amp;gt; list_value(Some(Arc::clone(&amp;amp;state.config.relay.admins)), None, pkey),&lt;br/&gt;            Nip86Request::RemoveAdmin {&lt;br/&gt;                params: PublicKeyParam(pkey),&lt;br/&gt;            } =&amp;gt; list_value(None, Some(Arc::clone(&amp;amp;state.config.relay.admins)), pkey),&lt;br/&gt;            Nip86Request::ListAllowedPubkeys { .. } =&amp;gt; {&lt;br/&gt;                list_values(Arc::clone(&amp;amp;state.config.relay.whitelist), |pkeys| {&lt;br/&gt;                    Nip86Result::PublicKeysAndReason(&lt;br/&gt;                        pkeys.into_iter().map(PubKeyWithReason::from).collect(),&lt;br/&gt;                    )&lt;br/&gt;                })&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ListBannedPubkeys { .. } =&amp;gt; {&lt;br/&gt;                list_values(Arc::clone(&amp;amp;state.config.relay.blacklist), |pkeys| {&lt;br/&gt;                    Nip86Result::PublicKeysAndReason(&lt;br/&gt;                        pkeys.into_iter().map(PubKeyWithReason::from).collect(),&lt;br/&gt;                    )&lt;br/&gt;                })&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ListAllowedKinds { .. } =&amp;gt; {&lt;br/&gt;                list_values(&lt;br/&gt;                    Arc::clone(&amp;amp;state.config.relay.allowed_kinds),&lt;br/&gt;                    Nip86Result::Kinds,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ListDisallowedKinds { .. } =&amp;gt; {&lt;br/&gt;                list_values(&lt;br/&gt;                    Arc::clone(&amp;amp;state.config.relay.disallowed_kinds),&lt;br/&gt;                    Nip86Result::Kinds,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;            Nip86Request::ListAdmins { .. } =&amp;gt; {&lt;br/&gt;                list_values(&lt;br/&gt;                    Arc::clone(&amp;amp;state.config.relay.admins),&lt;br/&gt;                    Nip86Result::PublicKeys,&lt;br/&gt;                )&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the request can only be performed by the super admin (the&lt;br/&gt;    /// first admin in the list).&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn only_superadmin(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        matches!(&lt;br/&gt;            self,&lt;br/&gt;            Self::AddAdmin { .. } | Self::RemoveAdmin { .. } | Self::ListAdmins { .. }&lt;br/&gt;        )&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Adds a value to either the whitelist or blacklist, perhaps :).&lt;br/&gt;fn list_value&amp;lt;T: PartialEq&amp;gt;(&lt;br/&gt;    add_to: Option&amp;lt;Arc&amp;lt;RwLock&amp;lt;Vec&amp;lt;T&amp;gt;&amp;gt;&amp;gt;&amp;gt;,&lt;br/&gt;    remove_from: Option&amp;lt;Arc&amp;lt;RwLock&amp;lt;Vec&amp;lt;T&amp;gt;&amp;gt;&amp;gt;&amp;gt;,&lt;br/&gt;    value: T,&lt;br/&gt;) -&amp;gt; ApiResult&amp;lt;Nip86Result&amp;gt; {&lt;br/&gt;    if let Some(remove_from) = remove_from {&lt;br/&gt;        let mut remove_from_lock = remove_from.write();&lt;br/&gt;        if remove_from_lock.contains(&amp;amp;value) {&lt;br/&gt;            remove_from_lock.retain(|p| p != &amp;amp;value)&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if let Some(add_to) = add_to {&lt;br/&gt;        let mut add_to_lock = add_to.write();&lt;br/&gt;        if !add_to_lock.contains(&amp;amp;value) {&lt;br/&gt;            add_to_lock.push(value);&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    Ok(Nip86Result::True)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// List values. Whitelist or blacklist, perhaps :)&lt;br/&gt;fn list_values&amp;lt;T: Copy&amp;gt;(&lt;br/&gt;    list_from: Arc&amp;lt;RwLock&amp;lt;Vec&amp;lt;T&amp;gt;&amp;gt;&amp;gt;,&lt;br/&gt;    result_fn: impl FnOnce(Vec&amp;lt;T&amp;gt;) -&amp;gt; Nip86Result,&lt;br/&gt;) -&amp;gt; ApiResult&amp;lt;Nip86Result&amp;gt; {&lt;br/&gt;    Ok(result_fn(list_from.read().iter().copied().collect()))&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:11Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsp3qmrymgfjtlhplyl0amlvj8v88u6jv4z894xr5rqguv37kk58tszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrnrt4v</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsp3qmrymgfjtlhplyl0amlvj8v88u6jv4z894xr5rqguv37kk58tszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrnrt4v" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::{Component, PathBuf};&lt;br/&gt;&lt;br/&gt;use axum::{extract::FromRequestParts, http::request::Parts};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;use nostr::key::PublicKey;&lt;br/&gt;&lt;br/&gt;/// Parameters containing a repository author&amp;#39;s public key and repository name.&lt;br/&gt;#[derive(serde::Deserialize)]&lt;br/&gt;pub struct PublicKeyAndRepoPath {&lt;br/&gt;    /// The author&amp;#39;s Nostr public key.&lt;br/&gt;    pub public_key: PublicKey,&lt;br/&gt;    /// The full repository name.&lt;br/&gt;    pub repo_name:  String,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Either `git-upload-pack` or `git-receive-pack`&lt;br/&gt;#[derive(serde::Deserialize)]&lt;br/&gt;#[serde(rename_all = &amp;#34;kebab-case&amp;#34;)]&lt;br/&gt;pub enum ServiceName {&lt;br/&gt;    GitUploadPack,&lt;br/&gt;    GitReceivePack,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Query parameter containing the service name (`?service=&amp;lt;string&amp;gt;`)&lt;br/&gt;#[derive(serde::Deserialize)]&lt;br/&gt;pub struct ServiceQuery {&lt;br/&gt;    /// The service name&lt;br/&gt;    pub service: ServiceName,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// An extractor that indecates what file to return its content, it&amp;#39;s used with&lt;br/&gt;/// `get_file_content` endpoint only.&lt;br/&gt;pub struct GitFilePath(pub PathBuf);&lt;br/&gt;&lt;br/&gt;impl ServiceName {&lt;br/&gt;    /// Gets the service name without the `git-` prefix&lt;br/&gt;    pub const fn name(&amp;amp;self) -&amp;gt; &amp;amp;&amp;#39;static str {&lt;br/&gt;        match self {&lt;br/&gt;            Self::GitUploadPack =&amp;gt; &amp;#34;upload-pack&amp;#34;,&lt;br/&gt;            Self::GitReceivePack =&amp;gt; &amp;#34;receive-pack&amp;#34;,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Gets the pkt-line header of the service&lt;br/&gt;    pub const fn pkt_line_header(&amp;amp;self) -&amp;gt; &amp;amp;[u8] {&lt;br/&gt;        match self {&lt;br/&gt;            Self::GitUploadPack =&amp;gt; &amp;#34;001e# service=git-upload-pack\n0000&amp;#34;.as_bytes(),&lt;br/&gt;            Self::GitReceivePack =&amp;gt; &amp;#34;001f# service=git-receive-pack\n0000&amp;#34;.as_bytes(),&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;S&amp;gt; FromRequestParts&amp;lt;S&amp;gt; for GitFilePath&lt;br/&gt;where&lt;br/&gt;    S: Send &#43; Sync,&lt;br/&gt;{&lt;br/&gt;    type Rejection = (StatusCode, &amp;amp;&amp;#39;static str);&lt;br/&gt;&lt;br/&gt;    async fn from_request_parts(parts: &amp;amp;mut Parts, _state: &amp;amp;S) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {&lt;br/&gt;        // Skip `/&amp;lt;npub&amp;gt;/repo.git/`&lt;br/&gt;        let path = PathBuf::from(&lt;br/&gt;            parts&lt;br/&gt;                .uri&lt;br/&gt;                .path()&lt;br/&gt;                .trim_matches(&amp;#39;/&amp;#39;)&lt;br/&gt;                .split(&amp;#39;/&amp;#39;)&lt;br/&gt;                .skip(2)&lt;br/&gt;                .collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;()&lt;br/&gt;                .join(&amp;#34;/&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        if path.components().any(|component| {&lt;br/&gt;            matches!(&lt;br/&gt;                component,&lt;br/&gt;                Component::CurDir | Component::ParentDir | Component::Prefix(..)&lt;br/&gt;            )&lt;br/&gt;        }) {&lt;br/&gt;            return Err((StatusCode::BAD_REQUEST, &amp;#34;Invalid path&amp;#34;));&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        if path.as_os_str() == &amp;#34;HEAD&amp;#34;&lt;br/&gt;            || path.starts_with(&amp;#34;objects/info/&amp;#34;)&lt;br/&gt;            || path.starts_with(&amp;#34;objects/pack/&amp;#34;)&lt;br/&gt;        {&lt;br/&gt;            Ok(Self(path.to_path_buf()))&lt;br/&gt;        } else {&lt;br/&gt;            Err((StatusCode::BAD_REQUEST, &amp;#34;Unknown path&amp;#34;))&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;S&amp;gt; FromRequestParts&amp;lt;S&amp;gt; for PublicKeyAndRepoPath&lt;br/&gt;where&lt;br/&gt;    S: Send &#43; Sync,&lt;br/&gt;{&lt;br/&gt;    type Rejection = (StatusCode, String);&lt;br/&gt;&lt;br/&gt;    async fn from_request_parts(parts: &amp;amp;mut Parts, state: &amp;amp;S) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {&lt;br/&gt;        use axum::extract::Path;&lt;br/&gt;&lt;br/&gt;        let Path(mut pkey_and_repo) =&lt;br/&gt;            Path::&amp;lt;PublicKeyAndRepoPath&amp;gt;::from_request_parts(parts, state)&lt;br/&gt;                .await&lt;br/&gt;                .map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;&lt;br/&gt;&lt;br/&gt;        if pkey_and_repo.repo_name.ends_with(&amp;#34;.git&amp;#34;) {&lt;br/&gt;            pkey_and_repo.repo_name = pkey_and_repo.repo_name.trim_end_matches(&amp;#34;.git&amp;#34;).to_owned();&lt;br/&gt;            return Ok(pkey_and_repo);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        Err((&lt;br/&gt;            StatusCode::BAD_REQUEST,&lt;br/&gt;            &amp;#34;The repository name must ends with `.git`&amp;#34;.to_owned(),&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:11Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsvtsp5lx4zhpgz9zhzcvd9jtfefdgc48n9ewq9jrm7v9tja0erenczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsytudp</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsvtsp5lx4zhpgz9zhzcvd9jtfefdgc48n9ewq9jrm7v9tja0erenczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsytudp" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use nostr::{event::Kind, key::PublicKey, types::Url};&lt;br/&gt;use serde::{Deserialize, Deserializer, Serialize, Serializer};&lt;br/&gt;&lt;br/&gt;/// Public key paired with an optional reason.&lt;br/&gt;#[derive(Serialize)]&lt;br/&gt;pub struct PubKeyWithReason {&lt;br/&gt;    #[serde(serialize_with = &amp;#34;hex_pubkey_ser&amp;#34;)]&lt;br/&gt;    pub pubkey: PublicKey,&lt;br/&gt;    pub reason: Option&amp;lt;String&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// A kind&lt;br/&gt;pub struct KindParam(pub Kind);&lt;br/&gt;&lt;br/&gt;/// A public key&lt;br/&gt;pub struct PublicKeyParam(pub PublicKey);&lt;br/&gt;&lt;br/&gt;/// A single string&lt;br/&gt;pub struct StringParam(pub String);&lt;br/&gt;&lt;br/&gt;/// A signle URL&lt;br/&gt;pub struct UrlParam(pub Url);&lt;br/&gt;&lt;br/&gt;impl From&amp;lt;PublicKey&amp;gt; for PubKeyWithReason {&lt;br/&gt;    fn from(pubkey: PublicKey) -&amp;gt; Self {&lt;br/&gt;        Self {&lt;br/&gt;            pubkey,&lt;br/&gt;            reason: None,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;de&amp;gt; Deserialize&amp;lt;&amp;#39;de&amp;gt; for PubKeyWithReason {&lt;br/&gt;    fn deserialize&amp;lt;D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Self, D::Error&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        D: Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        let params: Vec&amp;lt;String&amp;gt; = Vec::deserialize(deserializer)?;&lt;br/&gt;&lt;br/&gt;        let pubkey_str = params&lt;br/&gt;            .first()&lt;br/&gt;            .ok_or_else(|| de_err::&amp;lt;D&amp;gt;(&amp;#34;Missing required parameter: public key&amp;#34;))?;&lt;br/&gt;        let pubkey = PublicKey::parse(pubkey_str)&lt;br/&gt;            .map_err(|_| de_err::&amp;lt;D&amp;gt;(format!(&amp;#34;Invalid public key format: &amp;#39;{pubkey_str}&amp;#39;&amp;#34;)))?;&lt;br/&gt;        let reason = params.into_iter().nth(1);&lt;br/&gt;&lt;br/&gt;        Ok(Self { pubkey, reason })&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;de&amp;gt; Deserialize&amp;lt;&amp;#39;de&amp;gt; for KindParam {&lt;br/&gt;    fn deserialize&amp;lt;D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Self, D::Error&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        D: Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        Ok(Self(Kind::from(&lt;br/&gt;            *Vec::&amp;lt;u16&amp;gt;::deserialize(deserializer)?&lt;br/&gt;                .first()&lt;br/&gt;                .ok_or_else(|| de_err::&amp;lt;D&amp;gt;(&amp;#34;Missing required parameter: kind&amp;#34;))?,&lt;br/&gt;        )))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;de&amp;gt; Deserialize&amp;lt;&amp;#39;de&amp;gt; for StringParam {&lt;br/&gt;    fn deserialize&amp;lt;D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Self, D::Error&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        D: Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        Ok(Self(&lt;br/&gt;            Vec::&amp;lt;String&amp;gt;::deserialize(deserializer)?&lt;br/&gt;                .into_iter()&lt;br/&gt;                .next()&lt;br/&gt;                .ok_or_else(|| {&lt;br/&gt;                    de_err::&amp;lt;D&amp;gt;(&amp;#34;Expected at least one string in the list, but found none&amp;#34;)&lt;br/&gt;                })?,&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;de&amp;gt; Deserialize&amp;lt;&amp;#39;de&amp;gt; for PublicKeyParam {&lt;br/&gt;    fn deserialize&amp;lt;D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Self, D::Error&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        D: Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        Ok(Self(&lt;br/&gt;            PublicKey::parse(&amp;amp;StringParam::deserialize(deserializer)?.0)&lt;br/&gt;                .map_err(|_| de_err::&amp;lt;D&amp;gt;(&amp;#34;Invalid public key format&amp;#34;))?,&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;de&amp;gt; Deserialize&amp;lt;&amp;#39;de&amp;gt; for UrlParam {&lt;br/&gt;    fn deserialize&amp;lt;D&amp;gt;(deserializer: D) -&amp;gt; Result&amp;lt;Self, D::Error&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        D: Deserializer&amp;lt;&amp;#39;de&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        Ok(Self(&lt;br/&gt;            Url::parse(&amp;amp;StringParam::deserialize(deserializer)?.0)&lt;br/&gt;                .map_err(|_| de_err::&amp;lt;D&amp;gt;(&amp;#34;Invalid URL&amp;#34;))?,&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Converts the public key to a hex string for serialization.&lt;br/&gt;///&lt;br/&gt;/// Note: While this functionality is also provided by the `Serialize`&lt;br/&gt;/// implementation for `PublicKey`, it is included here to guard against&lt;br/&gt;/// potential breaking changes in the beta crate.&lt;br/&gt;pub fn hex_pubkey_ser&amp;lt;S: Serializer&amp;gt;(pubkey: &amp;amp;PublicKey, serializer: S) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt; {&lt;br/&gt;    pubkey.to_hex().serialize(serializer)&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// A function to return a custom deserializing error&lt;br/&gt;fn de_err&amp;lt;&amp;#39;de, D: Deserializer&amp;lt;&amp;#39;de&amp;gt;&amp;gt;(err: impl AsRef&amp;lt;str&amp;gt;) -&amp;gt; D::Error {&lt;br/&gt;    serde::de::Error::custom(err.as_ref())&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:00Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsgp3atrpvzwhqqxtxttpgg2t97gn5dgxu2vlq4s4585g85uc9gpdczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv087aj3</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsgp3atrpvzwhqqxtxttpgg2t97gn5dgxu2vlq4s4585g85uc9gpdczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv087aj3" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::time::Duration;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Router,&lt;br/&gt;    http::HeaderValue,&lt;br/&gt;    routing::{get, post},&lt;br/&gt;};&lt;br/&gt;use tower::limit::ConcurrencyLimitLayer;&lt;br/&gt;use tower_http::timeout::TimeoutLayer;&lt;br/&gt;&lt;br/&gt;use crate::relay_config::RelayConfig;&lt;br/&gt;&lt;br/&gt;/// Git endpoint to return a file content&lt;br/&gt;mod get_file_content;&lt;br/&gt;/// Git command&lt;br/&gt;pub mod git_command;&lt;br/&gt;/// Git `info/refs` endpoint&lt;br/&gt;mod info_refs;&lt;br/&gt;/// Path and query params&lt;br/&gt;mod path_and_query;&lt;br/&gt;/// Git receive pack endpoint&lt;br/&gt;mod receive_pack;&lt;br/&gt;/// Parses the ref update packet lines&lt;br/&gt;pub mod ref_update_pkt_line;&lt;br/&gt;/// Git upload pack endpoint&lt;br/&gt;mod upload_pack;&lt;br/&gt;/// Git utils&lt;br/&gt;pub mod utils;&lt;br/&gt;&lt;br/&gt;use get_file_content::get_file_content;&lt;br/&gt;use info_refs::info_refs;&lt;br/&gt;pub use path_and_query::*;&lt;br/&gt;use receive_pack::receive_pack;&lt;br/&gt;use upload_pack::upload_pack;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// The `Expires` header value to prevent caching (sets date far in the past)&lt;br/&gt;const EXPIRES_NO_CACHE: HeaderValue = HeaderValue::from_static(&amp;#34;Fri, 01 Jan 1980 00:00:00 GMT&amp;#34;);&lt;br/&gt;/// The `Cache-Control` header value to prevent caching&lt;br/&gt;const CACHE_CONTROL_NO_CACHE: HeaderValue =&lt;br/&gt;    HeaderValue::from_static(&amp;#34;no-cache, max-age=0, must-revalidate&amp;#34;);&lt;br/&gt;&lt;br/&gt;/// Creates a router with paths prefixed by `/{public_key}/{repo_name}`.&lt;br/&gt;///&lt;br/&gt;/// # Example&lt;br/&gt;/// ```rust&lt;br/&gt;/// use axum::routing::get;&lt;br/&gt;/// use axum::Router;&lt;br/&gt;/// use n34_relay::git_router;&lt;br/&gt;///&lt;br/&gt;/// # fn main() {&lt;br/&gt;/// let router: Router&amp;lt;()&amp;gt; = git_router!(&lt;br/&gt;///     &amp;#34;/git-upload-pack&amp;#34; =&amp;gt; get::&amp;lt;_, ((),), ()&amp;gt;(|| async {})&lt;br/&gt;///     &amp;#34;/git-receive-pack&amp;#34; =&amp;gt; get::&amp;lt;_, ((),), ()&amp;gt;(|| async {})&lt;br/&gt;/// );&lt;br/&gt;/// # }&lt;br/&gt;/// ```&lt;br/&gt;#[macro_export]&lt;br/&gt;macro_rules! git_router {&lt;br/&gt;    ($($path:tt =&amp;gt; $endpoint:expr)&#43;) =&amp;gt; {&lt;br/&gt;            axum::Router::new()&lt;br/&gt;        $(&lt;br/&gt;            .route(const { const_format::concatcp!(&amp;#34;/{public_key}/{repo_name}&amp;#34;, $path) }, $endpoint)&lt;br/&gt;        )&#43;&lt;br/&gt;    };&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Creates a router for git-related endpoints&lt;br/&gt;#[allow(deprecated)]&lt;br/&gt;pub fn router(config: &amp;amp;RelayConfig) -&amp;gt; Router {&lt;br/&gt;    let mut router = git_router!(&lt;br/&gt;        &amp;#34;/git-upload-pack&amp;#34; =&amp;gt; post(upload_pack)&lt;br/&gt;        &amp;#34;/git-receive-pack&amp;#34; =&amp;gt; post(receive_pack)&lt;br/&gt;        &amp;#34;/info/refs&amp;#34; =&amp;gt; get(info_refs)&lt;br/&gt;        &amp;#34;/HEAD&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;false&amp;gt;)&lt;br/&gt;        &amp;#34;/objects/info/packs&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;true&amp;gt;)&lt;br/&gt;        &amp;#34;/objects/info/{*rest}&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;false&amp;gt;)&lt;br/&gt;        &amp;#34;/objects/pack/{pack}&amp;#34; =&amp;gt; get(get_file_content::&amp;lt;true&amp;gt;)&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    if let Some(max_reqs) = config.grasp.max_reqs {&lt;br/&gt;        router = router.layer(ConcurrencyLimitLayer::new(max_reqs.into()));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    if let Some(timeout) = config.grasp.req_timeout {&lt;br/&gt;        router = router.layer(TimeoutLayer::new(Duration::from_secs(timeout.into())));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    router&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:21:00Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxwctycf09f68dn3rha93d3ru509hahrcffmmd03j9dgcclpdnfdgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvkwy2gs</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxwctycf09f68dn3rha93d3ru509hahrcffmmd03j9dgcclpdnfdgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvkwy2gs" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    extract::Query,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::{HeaderMap, StatusCode};&lt;br/&gt;&lt;br/&gt;use super::{ServiceQuery, git_command::GitCommand};&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;/// Retrieves Git repository reference information for a given service.&lt;br/&gt;/// Returns a response containing the references or an appropriate error message&lt;br/&gt;/// if the repository or service is not found.&lt;br/&gt;pub async fn info_refs(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    Query(ServiceQuery { service }): Query&amp;lt;ServiceQuery&amp;gt;,&lt;br/&gt;    headers: HeaderMap,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    match GitCommand::new(&amp;amp;state.config.grasp.git_path, &amp;amp;repo_path)&lt;br/&gt;        .refs(&amp;amp;service, super::utils::contains_git_v2(&amp;amp;headers))&lt;br/&gt;        .await&lt;br/&gt;    {&lt;br/&gt;        Ok(response_body) =&amp;gt; {&lt;br/&gt;            Response::builder()&lt;br/&gt;                .status(StatusCode::OK)&lt;br/&gt;                .header(&lt;br/&gt;                    &amp;#34;Content-Type&amp;#34;,&lt;br/&gt;                    format!(&amp;#34;application/x-git-{}-advertisement&amp;#34;, service.name()),&lt;br/&gt;                )&lt;br/&gt;                .header(&amp;#34;Pragma&amp;#34;, &amp;#34;no-cache&amp;#34;)&lt;br/&gt;                .header(&amp;#34;Cache-Control&amp;#34;, crate::git_server::CACHE_CONTROL_NO_CACHE)&lt;br/&gt;                .header(&amp;#34;Expires&amp;#34;, crate::git_server::EXPIRES_NO_CACHE)&lt;br/&gt;                .body(response_body)&lt;br/&gt;                .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;        }&lt;br/&gt;        Err(err_msg) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, err_msg).into_response(),&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:50Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsva8ku7x9w0emavdesz0n48m5f4zusxxhr7dstc5h7sw66hllqtcgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdhs0yg</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsva8ku7x9w0emavdesz0n48m5f4zusxxhr7dstc5h7sw66hllqtcgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdhs0yg" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    body::Bytes,&lt;br/&gt;    extract::{FromRequest, Request},&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use nostr::hashes::hex::DisplayHex;&lt;br/&gt;use sha1::Digest;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    endpoints::nip86::{&lt;br/&gt;        errors::{ApiError, ApiResult},&lt;br/&gt;        requests::Nip86Request,&lt;br/&gt;    },&lt;br/&gt;    ext_traits::RwlockVecExt,&lt;br/&gt;    router_state::RouterState,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// API errors&lt;br/&gt;mod errors;&lt;br/&gt;/// Requests parameters&lt;br/&gt;mod params;&lt;br/&gt;/// API requests&lt;br/&gt;mod requests;&lt;br/&gt;/// API responses&lt;br/&gt;mod responses;&lt;br/&gt;/// API utils&lt;br/&gt;mod utils;&lt;br/&gt;&lt;br/&gt;/// A main NIP86 API handler.&lt;br/&gt;pub async fn main_nip86_handler(state: Arc&amp;lt;RouterState&amp;gt;, request: Request) -&amp;gt; ApiResult&amp;lt;Response&amp;gt; {&lt;br/&gt;    let (request_author, payload_hash) = utils::get_payload_hash(&lt;br/&gt;        utils::get_auth_token(request.headers())?,&lt;br/&gt;        request.method(),&lt;br/&gt;        state.as_ref(),&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    let request_body = Bytes::from_request(request, &amp;amp;())&lt;br/&gt;        .await&lt;br/&gt;        .map_err(|err| ApiError::InvalidBody(err.to_string()))?;&lt;br/&gt;&lt;br/&gt;    if payload_hash != sha2::Sha256::digest(&amp;amp;request_body).as_hex().to_string() {&lt;br/&gt;        return Err(ApiError::IncorrectBodyHash);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let rpc_request: Nip86Request = serde_json::from_slice(&amp;amp;request_body).map_err(|err| {&lt;br/&gt;        ApiError::InvalidRpcRequest(utils::remove_line_number(&amp;amp;err.to_string()).to_owned())&lt;br/&gt;    })?;&lt;br/&gt;&lt;br/&gt;    if rpc_request.only_superadmin() &amp;amp;&amp;amp; !state.config.relay.admins.is_first(&amp;amp;request_author) {&lt;br/&gt;        return Err(ApiError::NotSuperAdmin);&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    rpc_request&lt;br/&gt;        .run(state)&lt;br/&gt;        .await&lt;br/&gt;        .map(IntoResponse::into_response)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:48Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs95nlv9hfkqdq3d6tc9lu6u8q3q42ex5c6z5gpvpwcjmqwccg29sszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvk8qrpn</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs95nlv9hfkqdq3d6tc9lu6u8q3q42ex5c6z5gpvpwcjmqwccg29sszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvk8qrpn" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{path::Path, process::Stdio};&lt;br/&gt;&lt;br/&gt;use axum::body::Body;&lt;br/&gt;use tokio::{&lt;br/&gt;    io::{AsyncReadExt, AsyncWriteExt},&lt;br/&gt;    process::{Child, ChildStderr, Command},&lt;br/&gt;};&lt;br/&gt;use tokio_util::io::ReaderStream;&lt;br/&gt;&lt;br/&gt;use crate::git_server::ServiceName;&lt;br/&gt;&lt;br/&gt;/// A helper for executing git commands with specific paths.&lt;br/&gt;pub struct GitCommand&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    git_path: &amp;amp;&amp;#39;a str,&lt;br/&gt;    git_repo: &amp;amp;&amp;#39;a Path,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl&amp;lt;&amp;#39;a&amp;gt; GitCommand&amp;lt;&amp;#39;a&amp;gt; {&lt;br/&gt;    /// Creates a new [`GitCommand`] instance with the given git path and&lt;br/&gt;    /// repository.&lt;br/&gt;    pub fn new(git_path: &amp;amp;&amp;#39;a str, git_repo: &amp;amp;&amp;#39;a Path) -&amp;gt; Self {&lt;br/&gt;        Self { git_path, git_repo }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Spawns a git process with the provided arguments.&lt;br/&gt;    fn spawn_git(&amp;amp;self, args: &amp;amp;[&amp;amp;str], v2: bool) -&amp;gt; Option&amp;lt;Child&amp;gt; {&lt;br/&gt;        let mut command = Command::new(self.git_path);&lt;br/&gt;        // from GRASP protocol:&lt;br/&gt;        // MUST include `allow-reachable-sha1-in-want` and&lt;br/&gt;        // `allow-tip-sha1-in-want` in advertisement and serve available oids.&lt;br/&gt;        command.args([&amp;#34;-c&amp;#34;, &amp;#34;uploadpack.allowTipSHA1InWant=true&amp;#34;]);&lt;br/&gt;        command.args([&amp;#34;-c&amp;#34;, &amp;#34;uploadpack.allowReachableSHA1InWant=true&amp;#34;]);&lt;br/&gt;&lt;br/&gt;        command.args(args);&lt;br/&gt;        command.stdin(Stdio::piped());&lt;br/&gt;        command.stdout(Stdio::piped());&lt;br/&gt;        command.stderr(Stdio::piped());&lt;br/&gt;        command.current_dir(self.git_repo);&lt;br/&gt;&lt;br/&gt;        if v2 {&lt;br/&gt;            command.env(&amp;#34;GIT_PROTOCOL&amp;#34;, &amp;#34;version=2&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        command.spawn().ok()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Sends a request body to a git command and returns its output as a&lt;br/&gt;    /// stream.&lt;br/&gt;    async fn pass_request_to_git(&lt;br/&gt;        &amp;amp;self,&lt;br/&gt;        args: &amp;amp;[&amp;amp;&amp;#39;static str],&lt;br/&gt;        body: &amp;amp;[u8],&lt;br/&gt;        v2: bool,&lt;br/&gt;    ) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let Some(mut process) = self.spawn_git(args, v2) else {&lt;br/&gt;            return Err(&amp;#34;Failed to run git command&amp;#34;);&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        let mut stdin = process.stdin.take().expect(&amp;#34;git stdin&amp;#34;);&lt;br/&gt;        if let Err(err) = stdin.write_all(body).await {&lt;br/&gt;            tracing::error!(args = ?args, error = %err, &amp;#34;Failed to write the request body to git stdin&amp;#34;);&lt;br/&gt;            return Err(&amp;#34;Failed to pass the request body to git&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;        drop(stdin);&lt;br/&gt;&lt;br/&gt;        let stderr = process.stderr.take().expect(&amp;#34;git stderr&amp;#34;);&lt;br/&gt;        let stdout = process.stdout.take().expect(&amp;#34;git stdout&amp;#34;);&lt;br/&gt;        wait_process(process, stderr);&lt;br/&gt;        Ok(Body::from_stream(ReaderStream::new(stdout)))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes a git command with the provided arguments and returns its&lt;br/&gt;    /// output. If a body is needed, use [Self::pass_request_to_git]&lt;br/&gt;    /// instead.&lt;br/&gt;    async fn call_git(&amp;amp;self, args: &amp;amp;[&amp;amp;&amp;#39;static str], v2: bool) -&amp;gt; Result&amp;lt;Vec&amp;lt;u8&amp;gt;, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let Some(process) = self.spawn_git(args, v2) else {&lt;br/&gt;            return Err(&amp;#34;Failed to run git command&amp;#34;);&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        process&lt;br/&gt;            .wait_with_output()&lt;br/&gt;            .await&lt;br/&gt;            .map(|o| o.stdout)&lt;br/&gt;            .map_err(|err| {&lt;br/&gt;                tracing::error!(args = ?args, error = %err, &amp;#34;Failed to get `git` output&amp;#34;);&lt;br/&gt;                &amp;#34;Failed to run `git` command&amp;#34;&lt;br/&gt;            })&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the `receive-pack` git command with the provided body.&lt;br/&gt;    pub async fn receive_pack(&amp;amp;self, body: &amp;amp;[u8]) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        self.pass_request_to_git(&amp;amp;[&amp;#34;receive-pack&amp;#34;, &amp;#34;--stateless-rpc&amp;#34;, &amp;#34;.&amp;#34;], body, false)&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the `upload-pack` git command with the provided body.&lt;br/&gt;    pub async fn upload_pack(&amp;amp;self, body: &amp;amp;[u8], v2: bool) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        self.pass_request_to_git(&amp;amp;[&amp;#34;upload-pack&amp;#34;, &amp;#34;--stateless-rpc&amp;#34;, &amp;#34;.&amp;#34;], body, v2)&lt;br/&gt;            .await&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Executes the given service git command with `--advertise-refs` argument.&lt;br/&gt;    pub async fn refs(&amp;amp;self, service: &amp;amp;ServiceName, v2: bool) -&amp;gt; Result&amp;lt;Body, &amp;amp;&amp;#39;static str&amp;gt; {&lt;br/&gt;        let body_bytes = self&lt;br/&gt;            .call_git(&lt;br/&gt;                &amp;amp;[service.name(), &amp;#34;--stateless-rpc&amp;#34;, &amp;#34;--advertise-refs&amp;#34;, &amp;#34;.&amp;#34;],&lt;br/&gt;                v2,&lt;br/&gt;            )&lt;br/&gt;            .await?;&lt;br/&gt;&lt;br/&gt;        Ok(Body::from(&lt;br/&gt;            [service.pkt_line_header(), body_bytes.as_ref()].concat(),&lt;br/&gt;        ))&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Spawns a task to wait for the process to complete and logs any errors.&lt;br/&gt;///&lt;br/&gt;/// If the process exits with non-zero status, reads and logs the stderr output.&lt;br/&gt;fn wait_process(mut process: Child, mut stderr: ChildStderr) {&lt;br/&gt;    tokio::spawn(async move {&lt;br/&gt;        let pid = process.id().unwrap_or_default();&lt;br/&gt;        match process.wait().await {&lt;br/&gt;            Ok(status) =&amp;gt; {&lt;br/&gt;                if !status.success() {&lt;br/&gt;                    let mut err = String::new();&lt;br/&gt;                    _ = stderr.read_to_string(&amp;amp;mut err).await;&lt;br/&gt;                    tracing::warn!(&lt;br/&gt;                        &amp;#34;Git process (PID {pid}) exited with non-zero status: {} ({status})&amp;#34;,&lt;br/&gt;                        err.trim(),&lt;br/&gt;                    );&lt;br/&gt;                }&lt;br/&gt;            }&lt;br/&gt;            Err(e) =&amp;gt; {&lt;br/&gt;                tracing::error!(&amp;#34;Error waiting for Git process (PID {pid}): {e}&amp;#34;);&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    });&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:38Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsqxsp7ax5sfttvensauw5gh0te4wqxxf0mlegjrt5jzv9ancg8m8qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq4zare</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsqxsp7ax5sfttvensauw5gh0te4wqxxf0mlegjrt5jzv9ancg8m8qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvq4zare" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use axum::response::{IntoResponse, Response};&lt;br/&gt;use hyper::StatusCode;&lt;br/&gt;use serde::Serialize;&lt;br/&gt;&lt;br/&gt;use crate::endpoints::nip86::responses::Nip86Response;&lt;br/&gt;&lt;br/&gt;/// API result type&lt;br/&gt;pub type ApiResult&amp;lt;T&amp;gt; = Result&amp;lt;T, ApiError&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// API errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum ApiError {&lt;br/&gt;    #[error(&amp;#34;Missing &amp;#39;Authorization&amp;#39; header in request&amp;#34;)]&lt;br/&gt;    NoAuthHeader,&lt;br/&gt;    #[error(&amp;#34;Invalid authorization header: contains non-ASCII characters&amp;#34;)]&lt;br/&gt;    InvalidAuthHeader,&lt;br/&gt;    #[error(&amp;#34;Authorization header must start with &amp;#39;Nostr &amp;#39; prefix&amp;#34;)]&lt;br/&gt;    NotNostrAuth,&lt;br/&gt;    #[error(&amp;#34;Invalid Base64 encoding in authorization token&amp;#34;)]&lt;br/&gt;    InvalidBase64Token,&lt;br/&gt;    #[error(&amp;#34;Invalid Nostr event in authorization token&amp;#34;)]&lt;br/&gt;    InvalidNostrEventToken,&lt;br/&gt;    #[error(&amp;#34;Access denied: administrator privileges required&amp;#34;)]&lt;br/&gt;    NotAdmin,&lt;br/&gt;    #[error(&amp;#34;Access denied: this request is restricted to super administrator only&amp;#34;)]&lt;br/&gt;    NotSuperAdmin,&lt;br/&gt;    #[error(&amp;#34;Invalid event ID in authorization token&amp;#34;)]&lt;br/&gt;    InvalidEventId,&lt;br/&gt;    #[error(&amp;#34;Authorization token must use event kind 27235&amp;#34;)]&lt;br/&gt;    NotHttpAuthKind,&lt;br/&gt;    #[error(&amp;#34;Authorization token event content must be empty&amp;#34;)]&lt;br/&gt;    NonEmptyHttpAuthContent,&lt;br/&gt;    #[error(&amp;#34;Authorization token has expired&amp;#34;)]&lt;br/&gt;    OldAuthEvent,&lt;br/&gt;    #[error(&amp;#34;Missing the relay url in authorization token&amp;#34;)]&lt;br/&gt;    MissingRlayUrl,&lt;br/&gt;    #[error(&amp;#34;Token relay URL does not match current relay URL&amp;#34;)]&lt;br/&gt;    IncorrectRelayUrl,&lt;br/&gt;    #[error(&amp;#34;Missing the request method in authorization token&amp;#34;)]&lt;br/&gt;    MissingMethod,&lt;br/&gt;    #[error(&amp;#34;Token request method does not match actual request method&amp;#34;)]&lt;br/&gt;    IncorrectRequestMethod,&lt;br/&gt;    #[error(&amp;#34;Invalid event signature in authorization token&amp;#34;)]&lt;br/&gt;    InvalidSignature,&lt;br/&gt;    #[error(&amp;#34;Missing payload hash in authorization token&amp;#34;)]&lt;br/&gt;    MissingPayloadHash,&lt;br/&gt;    #[error(&amp;#34;Invalid request body: {0}&amp;#34;)]&lt;br/&gt;    InvalidBody(String),&lt;br/&gt;    #[error(&amp;#34;Body hash does not match payload tag in authorization token&amp;#34;)]&lt;br/&gt;    IncorrectBodyHash,&lt;br/&gt;    #[error(&amp;#34;Invalid RPC request: {0}&amp;#34;)]&lt;br/&gt;    InvalidRpcRequest(String),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl ApiError {&lt;br/&gt;    /// Retrieves the HTTP status code associated with the error&lt;br/&gt;    #[inline]&lt;br/&gt;    pub fn status_code(&amp;amp;self) -&amp;gt; StatusCode {&lt;br/&gt;        match self {&lt;br/&gt;            Self::InvalidRpcRequest(_) =&amp;gt; StatusCode::BAD_REQUEST,&lt;br/&gt;            _ =&amp;gt; StatusCode::UNAUTHORIZED,&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl IntoResponse for ApiError {&lt;br/&gt;    fn into_response(self) -&amp;gt; Response {&lt;br/&gt;        Nip86Response::err_res(self).into_response()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl Serialize for ApiError {&lt;br/&gt;    fn serialize&amp;lt;S&amp;gt;(&amp;amp;self, serializer: S) -&amp;gt; Result&amp;lt;S::Ok, S::Error&amp;gt;&lt;br/&gt;    where&lt;br/&gt;        S: serde::Serializer,&lt;br/&gt;    {&lt;br/&gt;        self.to_string().serialize(serializer)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:35Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs8jjnplmmtae8w2492jhm29szz6x0cwxjzvhz5qj2wat4pcgcvwyqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvavgpc4</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs8jjnplmmtae8w2492jhm29szz6x0cwxjzvhz5qj2wat4pcgcvwyqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvavgpc4" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    body::Body,&lt;br/&gt;    http::HeaderValue,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;use hyper::{StatusCode, header};&lt;br/&gt;&lt;br/&gt;use crate::{git_server::GitFilePath, router_state::RouterState};&lt;br/&gt;&lt;br/&gt;/// Fetches the content of a file in a repository using the provided&lt;br/&gt;/// `GitFilePath`. The function checks if the repository and file exist, and&lt;br/&gt;/// handles caching headers based on the `CACHE_HEADER` flag.&lt;br/&gt;pub async fn get_file_content&amp;lt;const CACHE_HEADER: bool&amp;gt;(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    params: super::PublicKeyAndRepoPath,&lt;br/&gt;    GitFilePath(file_path): GitFilePath,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    let Some(repo_path) = state.repo_path(&amp;amp;params.public_key, &amp;amp;params.repo_name) else {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;Repository not found&amp;#34;).into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    let file_path = repo_path.join(file_path);&lt;br/&gt;&lt;br/&gt;    if !file_path.exists() {&lt;br/&gt;        return (StatusCode::NOT_FOUND, &amp;#34;File not found&amp;#34;).into_response();&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let mut response = Response::builder();&lt;br/&gt;    let headers = response&lt;br/&gt;        .headers_mut()&lt;br/&gt;        .expect(&amp;#34;builder function provide the response parts&amp;#34;);&lt;br/&gt;&lt;br/&gt;    if CACHE_HEADER {&lt;br/&gt;        let expires_value = (chrono::Utc::now() &#43; chrono::Duration::days(1)).to_rfc2822();&lt;br/&gt;        headers.insert(&lt;br/&gt;            &amp;#34;Cache-Control&amp;#34;,&lt;br/&gt;            HeaderValue::from_static(&amp;#34;public, max-age=86400&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;        headers.insert(&lt;br/&gt;            &amp;#34;Expires&amp;#34;,&lt;br/&gt;            HeaderValue::from_str(&amp;amp;expires_value).expect(&amp;#34;valid header value&amp;#34;),&lt;br/&gt;        );&lt;br/&gt;&lt;br/&gt;        let content_type = if file_path.extension().is_some_and(|ext| ext == &amp;#34;pack&amp;#34;) {&lt;br/&gt;            &amp;#34;application/x-git-packed-objects&amp;#34;&lt;br/&gt;        } else if file_path.extension().is_some_and(|ext| ext == &amp;#34;idx&amp;#34;) {&lt;br/&gt;            &amp;#34;application/x-git-packed-objects-toc&amp;#34;&lt;br/&gt;        } else {&lt;br/&gt;            &amp;#34;application/x-git-loose-object&amp;#34;&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));&lt;br/&gt;    } else {&lt;br/&gt;        headers.insert(&amp;#34;Pragma&amp;#34;, HeaderValue::from_static(&amp;#34;no-cache&amp;#34;));&lt;br/&gt;        headers.insert(&amp;#34;Cache-Control&amp;#34;, crate::git_server::CACHE_CONTROL_NO_CACHE);&lt;br/&gt;        headers.insert(&amp;#34;Expires&amp;#34;, crate::git_server::EXPIRES_NO_CACHE);&lt;br/&gt;        headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(&amp;#34;text/plain&amp;#34;));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let Ok(file_content) = tokio::fs::read(&amp;amp;file_path).await else {&lt;br/&gt;        return (&lt;br/&gt;            StatusCode::INTERNAL_SERVER_ERROR,&lt;br/&gt;            &amp;#34;Failed to get file content&amp;#34;,&lt;br/&gt;        )&lt;br/&gt;            .into_response();&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    Response::builder()&lt;br/&gt;        .status(StatusCode::OK)&lt;br/&gt;        .body(Body::from(file_content))&lt;br/&gt;        .expect(&amp;#34;valid response&amp;#34;)&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:27Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsd30e0m99w96r20t8dftlnexpl9v9xyr0k9wh2464chfj2sf5dftqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvk22sjj</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsd30e0m99w96r20t8dftlnexpl9v9xyr0k9wh2464chfj2sf5dftqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvk22sjj" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::sync::Arc;&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Json,&lt;br/&gt;    response::{IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::router_state::RouterState;&lt;br/&gt;&lt;br/&gt;pub async fn handler(state: Arc&amp;lt;RouterState&amp;gt;) -&amp;gt; Response {&lt;br/&gt;    Json(&amp;amp;state.config.nip11).into_response()&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:24Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs9gmd3hg3f6xy70x40uk53zdl87k6rkuy343e77exrqamcnjpwdlszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvuadcce</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs9gmd3hg3f6xy70x40uk53zdl87k6rkuy343e77exrqamcnjpwdlszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvuadcce" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{borrow::Cow, sync::Arc};&lt;br/&gt;&lt;br/&gt;use axum::http::HeaderValue;&lt;br/&gt;use hyper::{&lt;br/&gt;    HeaderMap,&lt;br/&gt;    header::{self, AsHeaderName},&lt;br/&gt;};&lt;br/&gt;use nostr::message::MachineReadablePrefix;&lt;br/&gt;use nostr_relay_builder::builder::WritePolicyResult;&lt;br/&gt;use parking_lot::RwLock;&lt;br/&gt;&lt;br/&gt;/// Extension trait for managing a shared list of things&lt;br/&gt;#[easy_ext::ext(RwlockVecExt)]&lt;br/&gt;pub impl&amp;lt;T: PartialEq&amp;gt; Arc&amp;lt;RwLock&amp;lt;Vec&amp;lt;T&amp;gt;&amp;gt;&amp;gt; {&lt;br/&gt;    /// Construct a new instance&lt;br/&gt;    fn new_empty() -&amp;gt; Self {&lt;br/&gt;        Arc::new(RwLock::new(Vec::new()))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Check if the list is empty&lt;br/&gt;    fn is_empty(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.read().is_empty()&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the given value exists in the list&lt;br/&gt;    fn contains(&amp;amp;self, val: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;        self.read().contains(val)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Returns `true` if the giveing value is the first value in the list&lt;br/&gt;    fn is_first(&amp;amp;self, value: &amp;amp;T) -&amp;gt; bool {&lt;br/&gt;        self.read()&lt;br/&gt;            .first()&lt;br/&gt;            .is_some_and(|first_value| first_value == value)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extension trait for `RwLock` options&lt;br/&gt;#[easy_ext::ext(RwlockOption)]&lt;br/&gt;pub impl&amp;lt;T&amp;gt; RwLock&amp;lt;Option&amp;lt;T&amp;gt;&amp;gt; {&lt;br/&gt;    /// Returns true if the option is none&lt;br/&gt;    fn is_none(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.read().is_none()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extension trait for header map&lt;br/&gt;#[easy_ext::ext(HeaderMapExt)]&lt;br/&gt;pub impl &amp;amp;HeaderMap&amp;lt;HeaderValue&amp;gt; {&lt;br/&gt;    const NOSTR_JSON_MIME: &amp;amp;&amp;#39;static str = &amp;#34;application/nostr&#43;json&amp;#34;;&lt;br/&gt;    const NOSTR_JSON_RPC_MIME: &amp;amp;&amp;#39;static str = &amp;#34;application/nostr&#43;json&#43;rpc&amp;#34;;&lt;br/&gt;    const UPGRADE_MIME: &amp;amp;&amp;#39;static str = &amp;#34;upgrade&amp;#34;;&lt;br/&gt;    const WEBSOCKET_MIME: &amp;amp;&amp;#39;static str = &amp;#34;websocket&amp;#34;;&lt;br/&gt;&lt;br/&gt;    /// Checks if the header map contains the specified header and if its value&lt;br/&gt;    /// matches the given value.&lt;br/&gt;    #[inline]&lt;br/&gt;    fn is_contains(&amp;amp;self, header_name: impl AsHeaderName, header_value: &amp;amp;str) -&amp;gt; bool {&lt;br/&gt;        self.get(header_name)&lt;br/&gt;            .and_then(|content_value| content_value.to_str().ok())&lt;br/&gt;            .is_some_and(|content_str| content_str.eq_ignore_ascii_case(header_value))&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the provided headers indicate an upgrade to a WebSocket&lt;br/&gt;    /// connection.&lt;br/&gt;    fn is_ws_upgrade(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.is_contains(header::CONNECTION, Self::UPGRADE_MIME)&lt;br/&gt;            &amp;amp;&amp;amp; self.is_contains(header::UPGRADE, Self::WEBSOCKET_MIME)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the provided headers indicate a NIP-86 request&lt;br/&gt;    fn is_nip86_req(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.is_contains(header::CONTENT_TYPE, Self::NOSTR_JSON_RPC_MIME)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    /// Checks if the provided headers indicate a NIP-11 request&lt;br/&gt;    fn is_nip11_req(&amp;amp;self) -&amp;gt; bool {&lt;br/&gt;        self.is_contains(header::ACCEPT, Self::NOSTR_JSON_MIME)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;/// Extension trait for [WritePolicyResult]&lt;br/&gt;#[easy_ext::ext(WritePolicyResultExt)]&lt;br/&gt;pub impl WritePolicyResult {&lt;br/&gt;    /// Reject result with `Blocked` prefix&lt;br/&gt;    #[inline]&lt;br/&gt;    fn blocked_reject&amp;lt;S&amp;gt;(msg: S) -&amp;gt; Self&lt;br/&gt;    where&lt;br/&gt;        S: Into&amp;lt;Cow&amp;lt;&amp;#39;static, str&amp;gt;&amp;gt;,&lt;br/&gt;    {&lt;br/&gt;        WritePolicyResult::reject(MachineReadablePrefix::Blocked, msg)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:15Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqswj0ld26uq26vvymsrxnfkl85nfl6u2vgamk4wr4dzlr9hlcsqymszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrs7emc</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqswj0ld26uq26vvymsrxnfkl85nfl6u2vgamk4wr4dzlr9hlcsqymszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvrs7emc" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::{&lt;br/&gt;    net::SocketAddr,&lt;br/&gt;    sync::{Arc, OnceLock},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use axum::{&lt;br/&gt;    Extension,&lt;br/&gt;    extract::{ConnectInfo, FromRequest, Request},&lt;br/&gt;    response::{Html, IntoResponse, Response},&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    ext_traits::HeaderMapExt,&lt;br/&gt;    raw_websocket::RawSocketUpgrade,&lt;br/&gt;    router_state::RouterState,&lt;br/&gt;    utils,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;/// Relay Information Document (NIP-11)&lt;br/&gt;mod nip11;&lt;br/&gt;/// Relay Management API (NIP-86)&lt;br/&gt;mod nip86;&lt;br/&gt;&lt;br/&gt;static HOME_PAGE_CONTENT: OnceLock&amp;lt;String&amp;gt; = OnceLock::new();&lt;br/&gt;&lt;br/&gt;/// Handles incoming requests.&lt;br/&gt;///&lt;br/&gt;/// routing them to the landing page, management API, or upgrading to a&lt;br/&gt;/// WebSocket connection for the relay.&lt;br/&gt;pub async fn main_handler(&lt;br/&gt;    Extension(state): Extension&amp;lt;Arc&amp;lt;RouterState&amp;gt;&amp;gt;,&lt;br/&gt;    ConnectInfo(addr): ConnectInfo&amp;lt;SocketAddr&amp;gt;,&lt;br/&gt;    req: Request,&lt;br/&gt;) -&amp;gt; Response {&lt;br/&gt;    if req.headers().is_ws_upgrade() {&lt;br/&gt;        match RawSocketUpgrade::from_request(req, &amp;amp;()).await {&lt;br/&gt;            Ok(ws) =&amp;gt; handle_ws(state, addr, ws),&lt;br/&gt;            Err(err) =&amp;gt; {&lt;br/&gt;                (&lt;br/&gt;                    axum::http::StatusCode::BAD_REQUEST,&lt;br/&gt;                    format!(&amp;#34;Failed to upgrade the connection: {err}&amp;#34;),&lt;br/&gt;                )&lt;br/&gt;                    .into_response()&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    } else if req.headers().is_nip86_req() {&lt;br/&gt;        nip86::main_nip86_handler(state, req).await.into_response()&lt;br/&gt;    } else if req.headers().is_nip11_req() {&lt;br/&gt;        self::nip11::handler(state).await&lt;br/&gt;    } else {&lt;br/&gt;        Html(&lt;br/&gt;            HOME_PAGE_CONTENT&lt;br/&gt;                .get_or_init(utils::homepage_content)&lt;br/&gt;                .as_str(),&lt;br/&gt;        )&lt;br/&gt;        .into_response()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;/// Pass the websocket connection to the relay&lt;br/&gt;pub fn handle_ws(state: Arc&amp;lt;RouterState&amp;gt;, addr: SocketAddr, ws: RawSocketUpgrade) -&amp;gt; Response {&lt;br/&gt;    ws.on_upgrade(async move |socket| {&lt;br/&gt;        if let Err(err) = state.relay.take_connection(socket, addr).await {&lt;br/&gt;            tracing::error!(addr = %addr, error = %err, &amp;#34;Failed to handle WebSocket connection&amp;#34;);&lt;br/&gt;        }&lt;br/&gt;    })&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:13Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsy2e2m6pc75mc2ts8kwck43zldwvawpw7ndzqe2varkvfwckxs29czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvayyw3u</id>
    
      <title type="html">// n34-relay - A nostr GRASP relay implementation // Copyright ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsy2e2m6pc75mc2ts8kwck43zldwvawpw7ndzqe2varkvfwckxs29czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvayyw3u" />
    <content type="html">
      // n34-relay - A nostr GRASP relay implementation&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU Affero General Public License as published&lt;br/&gt;// by the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU Affero General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU Affero General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/agpl-3.0&amp;gt&#34;&gt;https://gnu.org/licenses/agpl-3.0&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::path::PathBuf;&lt;br/&gt;&lt;br/&gt;use nostr_database::DatabaseError;&lt;br/&gt;&lt;br/&gt;use crate::relay::GrpcError;&lt;br/&gt;use crate::relay::RhaiPluginsError;&lt;br/&gt;&lt;br/&gt;/// Relay `Result` type&lt;br/&gt;pub type RelayResult&amp;lt;T&amp;gt; = Result&amp;lt;T, RelayError&amp;gt;;&lt;br/&gt;&lt;br/&gt;/// Relay errors&lt;br/&gt;#[derive(Debug, thiserror::Error)]&lt;br/&gt;pub enum RelayError {&lt;br/&gt;    #[error(&amp;#34;IO Error: {0}&amp;#34;)]&lt;br/&gt;    Io(#[from] std::io::Error),&lt;br/&gt;    #[error(&amp;#34;Database error: {0}&amp;#34;)]&lt;br/&gt;    Database(#[from] DatabaseError),&lt;br/&gt;    #[error(&amp;#34;gRPC initialization error: {0}&amp;#34;)]&lt;br/&gt;    Grpc(#[from] GrpcError),&lt;br/&gt;    #[error(&amp;#34;Rhai error: {0}&amp;#34;)]&lt;br/&gt;    Rhai(#[from] RhaiPluginsError),&lt;br/&gt;    #[error(&amp;#34;File system Error: path `{0}` {1}&amp;#34;)]&lt;br/&gt;    Fs(PathBuf, String),&lt;br/&gt;    #[error(&amp;#34;Config error: {0}&amp;#34;)]&lt;br/&gt;    Config(String),&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:20:01Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsd5jfvsv97nxh3we4xdyznvze8r0ta0enyhsqhcsxrjqz3dyptsjqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsve60q9l</id>
    
      <title type="html">#!/usr/bin/env sh N34_RELAY_BASE_DIR=./bin RUST_LOG=debug cargo ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsd5jfvsv97nxh3we4xdyznvze8r0ta0enyhsqhcsxrjqz3dyptsjqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsve60q9l" />
    <content type="html">
      #!/usr/bin/env sh&lt;br/&gt;N34_RELAY_BASE_DIR=./bin RUST_LOG=debug cargo run -p n34-relay&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:36Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs0m94g7e062hcz0pyymtwexm584nde2a9tt88snx2zh39yl3xwf2gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9yckl3</id>
    
      <title type="html">&amp;lt;svg width=&amp;#34;38&amp;#34; height=&amp;#34;38&amp;#34; viewBox=&amp;#34;0 0 ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs0m94g7e062hcz0pyymtwexm584nde2a9tt88snx2zh39yl3xwf2gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv9yckl3" />
    <content type="html">
      &amp;lt;svg width=&amp;#34;38&amp;#34; height=&amp;#34;38&amp;#34; viewBox=&amp;#34;0 0 38 38&amp;#34; fill=&amp;#34;none&amp;#34; xmlns=&amp;#34;&lt;a href=&#34;http://www.w3.org/2000/svg&amp;#34;&amp;gt&#34;&gt;http://www.w3.org/2000/svg&amp;#34;&amp;gt&lt;/a&gt;;&lt;br/&gt;&amp;lt;rect width=&amp;#34;38&amp;#34; height=&amp;#34;38&amp;#34; rx=&amp;#34;12&amp;#34; fill=&amp;#34;#4434FF&amp;#34;/&amp;gt;&lt;br/&gt;&amp;lt;path d=&amp;#34;M10.6731 30.6348C8.83687 30.6346 7.34885 29.1458 7.34885 27.3096C7.34891 26.2473 7.84783 25.303 8.62326 24.6943C8.21265 23.3055 7.86571 22.049 7.45334 20.6758C6.90247 18.8412 7.4492 16.8197 8.93576 15.5605L15.7512 9.78906C15.6931 9.54286 15.6614 9.28642 15.6613 9.02246C15.6613 7.51617 16.6628 6.24465 18.0363 5.83594L18.0363 -1.11215e-06C18.511 0.000462658 18.4612 0.000975391 18.9856 0.000975533C19.5102 0.000975578 19.5802 -1.11589e-06 19.9367 -9.46012e-07L19.9367 5.83594C21.3097 6.24503 22.3108 7.5166 22.3108 9.02246C22.3107 9.29118 22.2792 9.55249 22.219 9.80273L29.0783 15.6123C30.5229 16.8359 31.1022 18.8013 30.5539 20.6133L29.3254 24.6758C30.1142 25.2837 30.6232 26.2367 30.6233 27.3096C30.6233 29.1459 29.1344 30.6348 27.2981 30.6348C25.4619 30.6346 23.9738 29.1458 23.9738 27.3096C23.974 25.4734 25.4619 23.9846 27.2981 23.9844C27.3814 23.9844 27.4643 23.9891 27.5461 23.9951L28.7356 20.0625C29.0645 18.9753 28.7166 17.7966 27.8498 17.0625L21.2424 11.4648C20.8746 11.8048 20.4294 12.0622 19.9367 12.209L19.9367 18.9258C21.0425 19.3175 21.836 20.3694 21.8362 21.6094C21.8362 23.1834 20.5596 24.46 18.9856 24.46C17.4117 24.4598 16.136 23.1833 16.136 21.6094C16.1361 20.3689 16.93 19.3172 18.0363 18.9258L18.0363 12.21C17.5395 12.0622 17.0916 11.801 16.7219 11.457L10.1643 17.0107C9.27919 17.7605 8.93068 18.9867 9.27365 20.1289C9.68708 21.5056 10.0175 22.7009 10.3986 23.998C10.4892 23.9906 10.5806 23.9844 10.6731 23.9844C12.5093 23.9844 13.9981 25.4733 13.9983 27.3096C13.9983 29.1459 12.5094 30.6348 10.6731 30.6348Z&amp;#34; fill=&amp;#34;white&amp;#34;/&amp;gt;&lt;br/&gt;&amp;lt;/svg&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:25Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsyshycd5d7am0kwazx5vt5y8a4yyde33kwrjcwrrl6pdze0tc9uxgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfa6zfk</id>
    
      <title type="html">unstable_features = true style_edition = &amp;#34;2024&amp;#34; ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsyshycd5d7am0kwazx5vt5y8a4yyde33kwrjcwrrl6pdze0tc9uxgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfa6zfk" />
    <content type="html">
      unstable_features = true&lt;br/&gt;style_edition     = &amp;#34;2024&amp;#34;&lt;br/&gt;&lt;br/&gt;blank_lines_upper_bound        = 2&lt;br/&gt;combine_control_expr           = false&lt;br/&gt;wrap_comments                  = true&lt;br/&gt;condense_wildcard_suffixes     = true&lt;br/&gt;edition                        = &amp;#34;2024&amp;#34;&lt;br/&gt;enum_discrim_align_threshold   = 20&lt;br/&gt;force_multiline_blocks         = true&lt;br/&gt;format_code_in_doc_comments    = true&lt;br/&gt;format_generated_files         = false&lt;br/&gt;format_macro_matchers          = true&lt;br/&gt;format_strings                 = true&lt;br/&gt;imports_layout                 = &amp;#34;HorizontalVertical&amp;#34;&lt;br/&gt;newline_style                  = &amp;#34;Unix&amp;#34;&lt;br/&gt;normalize_comments             = true&lt;br/&gt;reorder_impl_items             = true&lt;br/&gt;group_imports                  = &amp;#34;StdExternalCrate&amp;#34;&lt;br/&gt;single_line_let_else_max_width = 0&lt;br/&gt;struct_field_align_threshold   = 20&lt;br/&gt;use_try_shorthand              = true&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:25Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsvksap0dl9m7e6tnlvgp6w53jm59zuhm8gtvm89hv6legy93a8fvczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvqccztg</id>
    
      <title type="html">// This protobuf contract defines the interface for managing and ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsvksap0dl9m7e6tnlvgp6w53jm59zuhm8gtvm89hv6legy93a8fvczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvqccztg" />
    <content type="html">
      // This protobuf contract defines the interface for managing and interacting&lt;br/&gt;// with plugins in a server.&lt;br/&gt;//&lt;br/&gt;// Plugins can be of two types: `WRITE` (for processing events) or `QUERY` (for&lt;br/&gt;// processing filters). Each plugin has a priority level (`ALL` or `ANY`) that&lt;br/&gt;// determines its execution behavior:&lt;br/&gt;// - `ALL`: The plugin must process the event/query.&lt;br/&gt;// - `ANY`: The plugin can skip processing if another plugin accepts the&lt;br/&gt;// event/query.&lt;br/&gt;&lt;br/&gt;syntax = &amp;#34;proto3&amp;#34;;&lt;br/&gt;package plugins;&lt;br/&gt;&lt;br/&gt;// Empty rpc request.&lt;br/&gt;message Empty {}&lt;br/&gt;&lt;br/&gt;// Represents the type of plugin, either for writing or querying.&lt;br/&gt;enum PluginType {&lt;br/&gt;  WRITE = 0;&lt;br/&gt;  QUERY = 1;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Defines the priority level for plugin event/query handling.&lt;br/&gt;//&lt;br/&gt;// `ALL` ensures the plugin must process the event/query.&lt;br/&gt;// `ANY` allows the plugin to skip processing if another plugin accepts the&lt;br/&gt;// event.&lt;br/&gt;enum PluginPriority {&lt;br/&gt;  // It must accept the event&lt;br/&gt;  ALL = 0;&lt;br/&gt;  // Plugins with this priority can skip processing if another plugin accepts&lt;br/&gt;  // the event. Commonly used for whitelisting purposes.&lt;br/&gt;  ANY = 1;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Information about a plugin.&lt;br/&gt;message PluginInfo {&lt;br/&gt;  // Unique name used to identify and call the plugin.&lt;br/&gt;  string name = 1;&lt;br/&gt;  // Type of plugin indicating when it should be executed.&lt;br/&gt;  PluginType plugin_type = 2;&lt;br/&gt;  // Priority level determining plugin execution order.&lt;br/&gt;  PluginPriority priority = 3;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Event tag `[&amp;#34;p&amp;#34;, &amp;#34;&amp;lt;pubkey&amp;gt;&amp;#34;, &amp;#34;&amp;lt;relay-url&amp;gt;&amp;#34;]`&lt;br/&gt;message Tag {&lt;br/&gt;  // Tag kind. e.g. &amp;#34;p&amp;#34;&lt;br/&gt;  string tag_kind = 1;&lt;br/&gt;  // Tag values. e.g. `[&amp;#34;&amp;lt;pubkey&amp;gt;&amp;#34;, &amp;#34;&amp;lt;relay-url&amp;gt;&amp;#34;]`&lt;br/&gt;  repeated string values = 2;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Query filter&lt;br/&gt;message Filter {&lt;br/&gt;  repeated string ids = 1;&lt;br/&gt;  repeated string authors = 2;&lt;br/&gt;  repeated uint32 kinds = 3;&lt;br/&gt;  repeated Tag tags = 4;&lt;br/&gt;  optional uint64 since = 5;&lt;br/&gt;  optional uint64 until = 6;&lt;br/&gt;  optional uint64 limit = 7;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Nostr event&lt;br/&gt;message Event {&lt;br/&gt;  // Event id&lt;br/&gt;  string id = 1;&lt;br/&gt;  // Author&amp;#39;s public key. In hex&lt;br/&gt;  string public_key = 2;&lt;br/&gt;  // Event created at&lt;br/&gt;  uint64 created_at = 3;&lt;br/&gt;  // Event kind&lt;br/&gt;  uint32 kind = 4;&lt;br/&gt;  // Event tags&lt;br/&gt;  repeated Tag tags = 5;&lt;br/&gt;  // Event content&lt;br/&gt;  string content = 6;&lt;br/&gt;  // Event signature&lt;br/&gt;  string signature = 7;&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Represents the plugins available on the server.&lt;br/&gt;message ServicePlugins { repeated PluginInfo plugins = 1; }&lt;br/&gt;&lt;br/&gt;// Represents a request to a plugin, where the plugin type determines the&lt;br/&gt;// content. Query plugins receive a filter, while write plugins receive an event.&lt;br/&gt;message PluginRequest {&lt;br/&gt;  // The plugin that should be run.&lt;br/&gt;  string plugin_name = 1;&lt;br/&gt;  // The plugin request, which must contain either a filter for query plugins or&lt;br/&gt;  // an event for write plugins.&lt;br/&gt;  oneof plugin_request_body {&lt;br/&gt;    // A filter request sent to query-type plugins.&lt;br/&gt;    Filter filter = 2;&lt;br/&gt;    // An event request sent to write-type plugins.&lt;br/&gt;    Event event = 3;&lt;br/&gt;  }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// Represents a response from a plugin, indicating whether an event or query is&lt;br/&gt;// accepted or rejected.&lt;br/&gt;//&lt;br/&gt;// If the plugin accepts the event or query, `accept` is used without additional data.&lt;br/&gt;// If the plugin rejects it, `reject_msg` contains the reason for the rejection.&lt;br/&gt;message PluginResponse {&lt;br/&gt;  // Indicates whether the event or query is accepted or rejected.&lt;br/&gt;  oneof plugin_response_body {&lt;br/&gt;    // Indicates that the event or query is accepted.&lt;br/&gt;    Empty accept = 1;&lt;br/&gt;    // Provides the reason for rejecting the event or query.&lt;br/&gt;    string reject_msg = 2;&lt;br/&gt;  }&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;service PluginsService {&lt;br/&gt;  // Returns the service plugins&lt;br/&gt;  rpc GetPlugins(Empty) returns (ServicePlugins);&lt;br/&gt;  // Run a plugin&lt;br/&gt;  rpc RunPlugin(PluginRequest) returns (PluginResponse);&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:14Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsyu0zyyterhezc8nndfzzg7f6gpgc3gmkkztcu2hh0cw3nfugj6pgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvqydh4g</id>
    
      <title type="html"># Grasp - Git Relays Authorized via Signed-Nostr Proofs Status: ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsyu0zyyterhezc8nndfzzg7f6gpgc3gmkkztcu2hh0cw3nfugj6pgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvqydh4g" />
    <content type="html">
      # Grasp - Git Relays Authorized via Signed-Nostr Proofs&lt;br/&gt;&lt;br/&gt;Status: DRAFT - expect breaking changes&lt;br/&gt;&lt;br/&gt;Contributions to open-source projects shouldn&amp;#39;t be permissioned by a platform like GitHub. Git repoistory hosting should be distributed. Grasp is a protocol like blossom, but for git.&lt;br/&gt;&lt;br/&gt;## Overview&lt;br/&gt;&lt;br/&gt;There may be many grasp servers anywhere -- like Blossom servers -- that host repositories from anyone (maybe they&amp;#39;ll ask for a pre-payment, maybe they will have a free quota for some Nostr users and so on) that you can just push your repositories to. And your pushes are pre-authorized by publishing a Nostr event beforehand that says what is your repository state (branch=commit, HEAD=branch or something like that).&lt;br/&gt;&lt;br/&gt;Then when announcing your repository you can include multiple git&#43;http URLs to these servers that people can clone the project from. And Git-enabled Nostr clients can contact these servers to download and display source code and Git history data.&lt;br/&gt;&lt;br/&gt;## Specification&lt;br/&gt;&lt;br/&gt;GRASP-01 is required. Everything else is optional.&lt;br/&gt;&lt;br/&gt;* GRASP-01 - Core Service Requirements&lt;br/&gt;* GRASP-02 - Proactive Sync&lt;br/&gt;* GRASP-05 - Archive&lt;br/&gt;&lt;br/&gt;Reference implementation - [ngit-relay](&lt;a href=&#34;https://gitworkshop.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-relay&#34;&gt;https://gitworkshop.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-relay&lt;/a&gt;)&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;TODO:&lt;br/&gt;- Service Announcements and Discovery&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:14Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsdgzxnrf8t7ahmzuky6xn8re4pxa8s3qq5qd4f7krw0ry07zp7g4gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfse00q</id>
    
      <title type="html">&amp;lt;svg width=&amp;#34;38&amp;#34; height=&amp;#34;38&amp;#34; viewBox=&amp;#34;0 0 ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsdgzxnrf8t7ahmzuky6xn8re4pxa8s3qq5qd4f7krw0ry07zp7g4gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfse00q" />
    <content type="html">
      &amp;lt;svg width=&amp;#34;38&amp;#34; height=&amp;#34;38&amp;#34; viewBox=&amp;#34;0 0 38 38&amp;#34; fill=&amp;#34;none&amp;#34; xmlns=&amp;#34;&lt;a href=&#34;http://www.w3.org/2000/svg&amp;#34;&amp;gt&#34;&gt;http://www.w3.org/2000/svg&amp;#34;&amp;gt&lt;/a&gt;;&lt;br/&gt;&amp;lt;g clip-path=&amp;#34;url(#clip0_10637_22944)&amp;#34;&amp;gt;&lt;br/&gt;&amp;lt;path d=&amp;#34;M8.97956 38C6.77048 37.9998 4.9797 36.2091 4.97956 34C4.97956 32.723 5.57913 31.5869 6.51081 30.8545C6.01667 29.1832 5.60276 27.6701 5.10651 26.0176C4.44377 23.8104 5.10116 21.3791 6.88972 19.8643L15.0889 12.9209C15.0189 12.6246 14.9806 12.3158 14.9805 11.998C14.9805 10.1857 16.1862 8.65565 17.8389 8.16406L17.8389 1.14258C17.8567 0.428463 18.368 -1.23664e-06 18.9991 -1.23664e-06C19.6302 6.24476e-06 20.1239 0.428464 20.1241 1.14258L20.1241 8.16504C21.7762 8.65704 22.9815 10.1861 22.9815 11.998C22.9815 12.3216 22.9427 12.6362 22.8702 12.9375L31.1231 19.9268C32.861 21.3988 33.559 23.7624 32.8995 25.9424L31.421 30.8301C32.3701 31.5615 32.9825 32.709 32.9825 34C32.9823 36.2092 31.1908 38 28.9815 38C26.7724 37.9998 24.9817 36.2091 24.9815 34C24.9815 31.7908 26.7724 29.9993 28.9815 29.999C29.0819 29.999 29.1818 30.0054 29.2803 30.0127L30.711 25.2803C31.1067 23.9723 30.6884 22.5541 29.6456 21.6709L21.6944 14.9375C21.2519 15.3462 20.7166 15.6556 20.1241 15.832L20.1241 23.9092C21.4555 24.38 22.4101 25.649 22.4102 27.1416C22.4102 29.0353 20.8743 30.5713 18.9805 30.5713C17.087 30.5711 15.5518 29.0352 15.5518 27.1416C15.552 25.6486 16.507 24.3797 17.8389 23.9092L17.8389 15.832C17.2413 15.6543 16.7027 15.3405 16.2579 14.9268L8.36726 21.6084C7.30238 22.5104 6.88333 23.9861 7.29597 25.3603C7.79328 27.0164 8.18707 28.4553 8.64558 30.0156C8.75577 30.0065 8.86702 29.999 8.97956 29.999C11.1889 29.999 12.9805 31.7907 12.9805 34C12.9804 36.2092 11.1888 38 8.97956 38Z&amp;#34; fill=&amp;#34;#4434FF&amp;#34;/&amp;gt;&lt;br/&gt;&amp;lt;/g&amp;gt;&lt;br/&gt;&amp;lt;defs&amp;gt;&lt;br/&gt;&amp;lt;clipPath id=&amp;#34;clip0_10637_22944&amp;#34;&amp;gt;&lt;br/&gt;&amp;lt;rect width=&amp;#34;38&amp;#34; height=&amp;#34;38&amp;#34; fill=&amp;#34;white&amp;#34;/&amp;gt;&lt;br/&gt;&amp;lt;/clipPath&amp;gt;&lt;br/&gt;&amp;lt;/defs&amp;gt;&lt;br/&gt;&amp;lt;/svg&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:01Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsyz2c0qnhdreql2n47j57uep3calh8n3fwhqslu9v4d4n269wyx4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvaytgam</id>
    
      <title type="html">MIT License Copyright (c) 2025 DanConwayDev Permission is hereby ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsyz2c0qnhdreql2n47j57uep3calh8n3fwhqslu9v4d4n269wyx4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvaytgam" />
    <content type="html">
      MIT License&lt;br/&gt;&lt;br/&gt;Copyright (c) 2025 DanConwayDev&lt;br/&gt;&lt;br/&gt;Permission is hereby granted, free of charge, to any person obtaining a copy&lt;br/&gt;of this software and associated documentation files (the &amp;#34;Software&amp;#34;), to deal&lt;br/&gt;in the Software without restriction, including without limitation the rights&lt;br/&gt;to use, copy, modify, merge, publish, distribute, sublicense, and/or sell&lt;br/&gt;copies of the Software, and to permit persons to whom the Software is&lt;br/&gt;furnished to do so, subject to the following conditions:&lt;br/&gt;&lt;br/&gt;The above copyright notice and this permission notice shall be included in all&lt;br/&gt;copies or substantial portions of the Software.&lt;br/&gt;&lt;br/&gt;THE SOFTWARE IS PROVIDED &amp;#34;AS IS&amp;#34;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR&lt;br/&gt;IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,&lt;br/&gt;FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE&lt;br/&gt;AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER&lt;br/&gt;LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,&lt;br/&gt;OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE&lt;br/&gt;SOFTWARE.&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:17:01Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsz8adea0lwwayw7fme984mddltrj89nmxl3ynzhsx23eraa4vzmcqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvnx22jm</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsz8adea0lwwayw7fme984mddltrj89nmxl3ynzhsx23eraa4vzmcqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvnx22jm" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{Cli, CliOptions, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;options&amp;#34;)&lt;br/&gt;            .required(true)&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct KeyringArgs {&lt;br/&gt;    /// Turns on secret key keyring. Requires entering the key once when&lt;br/&gt;    /// enabled.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    enable:  bool,&lt;br/&gt;    /// Turns off secret key keyring. Removes any existing key and prevents&lt;br/&gt;    /// storing new ones.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    disable: bool,&lt;br/&gt;    /// Deletes current key and stores the next provided key.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    reset:   bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for KeyringArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;&lt;br/&gt;        if self.enable {&lt;br/&gt;            options.config.keyring_secret_key = true;&lt;br/&gt;        } else if self.disable {&lt;br/&gt;            options.config.keyring_secret_key = false;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if self.reset || self.disable {&lt;br/&gt;            let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:04:25Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsfqszq70jn6c09c4rdsxwf4vpn0h798devsxmujtzn0k4mu9c08rczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdeceyx</id>
    
      <title type="html">#!/usr/bin/env sh cargo run --bin n34 -- repo view ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsfqszq70jn6c09c4rdsxwf4vpn0h798devsxmujtzn0k4mu9c08rczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdeceyx" />
    <content type="html">
      #!/usr/bin/env sh&lt;br/&gt;cargo run --bin n34 -- repo view naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpp4mhxue69uhkummn9ekx7mqppamhxue69uhkummnw3ezumt0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qg5waehxw309ahx7um5wghx77r5wghxgetkxpx8xj&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:04:03Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqstyjyg78watsswr954k682rfz4umjc9ya6eex5kwgssrmyyhwrqxgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyzrufz</id>
    
      <title type="html"># `build.rs` Documentation This document explains the ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqstyjyg78watsswr954k682rfz4umjc9ya6eex5kwgssrmyyhwrqxgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvyzrufz" />
    <content type="html">
      # `build.rs` Documentation&lt;br/&gt;&lt;br/&gt;This document explains the functionality of the `build.rs` script in this project. The `build.rs` script is a special Rust file that, if present, Cargo will compile and run *before* compiling the rest of your package. It&amp;#39;s typically used for tasks that need to be performed during the build process, such as generating code, setting environment variables, or performing conditional compilation.&lt;br/&gt;&lt;br/&gt;## Core Functionality&lt;br/&gt;&lt;br/&gt;The `build.rs` script in this project performs the following key functions:&lt;br/&gt;&lt;br/&gt;1.  **Environment Variable Injection:** It computes various project-related values at compile time and injects them as environment variables (`CARGO_RUSTC_ENV=...`) that can be accessed by the main crate using `env!(&amp;#34;VAR_NAME&amp;#34;)`. This includes:&lt;br/&gt;    *   `CARGO_PKG_NAME`: The name of the current package (from `Cargo.toml`).&lt;br/&gt;    *   `CARGO_PKG_VERSION`: The version of the current package (from `Cargo.toml`).&lt;br/&gt;    *   `GIT_COMMIT_HASH`: The full commit hash of the current Git HEAD (if in a Git repository).&lt;br/&gt;    *   `GIT_BRANCH`: The name of the current Git branch (if in a Git repository).&lt;br/&gt;    *   `CARGO_TOML_HASH`: The SHA-256 hash of the `Cargo.toml` file.&lt;br/&gt;    *   `LIB_HASH`: The SHA-256 hash of the `src/lib.rs` file.&lt;br/&gt;    *   `BUILD_HASH`: The SHA-256 hash of the `build.rs` file itself.&lt;br/&gt;&lt;br/&gt;2.  **Rerun Conditions:** It tells Cargo when to re-run the build script. This ensures that the injected environment variables and any conditional compilation logic are up-to-date if relevant files change:&lt;br/&gt;    *   `Cargo.toml`&lt;br/&gt;    *   `src/lib.rs`&lt;br/&gt;    *   `build.rs`&lt;br/&gt;    *   `.git/HEAD` (to detect changes in the Git repository like new commits or branch switches).&lt;br/&gt;    *   `src/get_file_hash_core/src/online_relays_gps.csv` (conditionally, if the file exists).&lt;br/&gt;&lt;br/&gt;3.  **Conditional Nostr Event Publishing (Release Builds with `nostr` feature):**&lt;br/&gt;    If the project is being compiled in **release mode (`--release`)** and the **`nostr` feature is enabled (`--features nostr`)**, the `build.rs` script will connect to Nostr relays and publish events. This is intended for &amp;#34;deterministic Nostr event build examples&amp;#34; as indicated by the comments in the file.&lt;br/&gt;&lt;br/&gt;    *   **Relay Management:** It retrieves a list of default relay URLs. During event publishing, it identifies and removes &amp;#34;unfriendly&amp;#34; or unresponsive relays (e.g., those with timeout, connection issues, or spam blocks) from the list for subsequent publications.&lt;br/&gt;    *   **File Hashing and Key Generation:** For each Git-tracked file (when in a Git repository), it computes its SHA-256 hash. This hash is then used to derive a Nostr `SecretKey`.&lt;br/&gt;    *   **Event Creation:**&lt;br/&gt;        *   **Individual File Events:** For each Git-tracked file, a Nostr `text_note` event is created. This event includes tags for:&lt;br/&gt;            *   `#file`: The path of the file.&lt;br/&gt;            *   `#version`: The package version.&lt;br/&gt;            *   `#commit`: The Git commit hash (if in a Git repository).&lt;br/&gt;            *   `#branch`: The Git branch name (if in a Git repository).&lt;br/&gt;        *   **Metadata Event:** It publishes a metadata event using `get_file_hash_core::publish_metadata_event`.&lt;br/&gt;        *   **Linking Event (Build Manifest):** After processing all individual files, if any events were published, a final &amp;#34;build manifest&amp;#34; `text_note` event is created. This event links to all the individual file events that were published during the build using event tags.&lt;br/&gt;    *   **Output Storage:** The JSON representation of successfully published Nostr events (specifically the `EventId`) is saved to `~/.gnostr/build/{package_version}/{file_path_str_sanitized}/{hash}/{public_key}/{event_id}.json`. This provides a local record of what was published.&lt;br/&gt;&lt;br/&gt;### `publish_nostr_event_if_release` Function&lt;br/&gt;&lt;br/&gt;This asynchronous helper function is responsible for:&lt;br/&gt;*   Adding relays to the Nostr client.&lt;br/&gt;*   Connecting to relays.&lt;br/&gt;*   Signing the provided `EventBuilder` to create an `Event`.&lt;br/&gt;*   Sending the event to the configured relays.&lt;br/&gt;*   Logging success or failure for each relay.&lt;br/&gt;*   Identifying and removing unresponsive relays from the `relay_urls` list.&lt;br/&gt;*   Saving the published event&amp;#39;s JSON to the local filesystem.&lt;br/&gt;&lt;br/&gt;### `should_remove_relay` Function&lt;br/&gt;&lt;br/&gt;This helper function determines if a relay should be considered &amp;#34;unfriendly&amp;#34; or unresponsive based on common error messages received during Nostr event publication.&lt;br/&gt;&lt;br/&gt;## Usage&lt;br/&gt;&lt;br/&gt;To prevent &amp;#39;Too many open files&amp;#39; errors, especially during builds and tests involving numerous file operations or subprocesses (like `git ls-files` or parallel test execution), it may be necessary to increase the file descriptor limit.&lt;br/&gt;&lt;br/&gt;*   **For local development**: Run `ulimit -n 4096` in your terminal session before executing `cargo build` or `cargo test`. This setting is session-specific.&lt;br/&gt;*   **For CI environments**: The `.github/workflows/rust.yml` workflow is configured to set `ulimit -n 4096` for relevant test steps to ensure consistent execution.&lt;br/&gt;&lt;br/&gt;The values set by `build.rs` can be accessed in your Rust code (e.g., `src/lib.rs`) at compile time using the `env!` macro. For example:&lt;br/&gt;```rust&lt;br/&gt;pub const CARGO_PKG_VERSION: &amp;amp;str = env!(&amp;#34;CARGO_PKG_VERSION&amp;#34;);&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;The Nostr event publishing functionality of `build.rs` is primarily for release builds with the `nostr` feature enabled, allowing for the automatic, deterministic publication of project state to the Nostr network as part of the CI/CD pipeline.&lt;br/&gt;&lt;br/&gt;## Example Commands&lt;br/&gt;&lt;br/&gt;To interact with the `build.rs` script&amp;#39;s features, especially those related to Nostr event publishing, you can use the following `cargo` commands:&lt;br/&gt;&lt;br/&gt;*   **Build in release mode with Nostr feature (verbose output):**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo build --release --workspace --features nostr -vv&lt;br/&gt;    ```&lt;br/&gt;&lt;br/&gt;*   **Run tests for `get_file_hash_core` sequentially with Nostr feature and verbose logging (as in CI):**&lt;br/&gt;    ```bash&lt;br/&gt;    RUST_LOG=info,nostr_sdk=debug,frost=debug cargo test -p get_file_hash_core --features nostr -- --test-threads 1 --nocapture&lt;br/&gt;    ```&lt;br/&gt;&lt;br/&gt;*   **Run all workspace tests in release mode with Nostr feature:**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo test --workspace --release --features nostr&lt;br/&gt;    ```&lt;br/&gt;&lt;br/&gt;*   **Build `get_file_hash_core` in release mode with Nostr feature (very verbose output):**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo build --release --features nostr -vv -p get_file_hash_core&lt;br/&gt;    ```&lt;br/&gt;&lt;br/&gt;*   **Run `get_file_hash_core` tests in release mode with Nostr feature (very verbose output):**&lt;br/&gt;    ```bash&lt;br/&gt;    cargo test --release --features nostr -vv -p get_file_hash_core&lt;br/&gt;    ```&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:03:58Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsd5attuq7eaae70mgp8w8p55pnng0wmnpfu9dyfhezenjl4el4xwgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc3gkxw</id>
    
      <title>Nostr event nevent1qqsd5attuq7eaae70mgp8w8p55pnng0wmnpfu9dyfhezenjl4el4xwgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc3gkxw</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsd5attuq7eaae70mgp8w8p55pnng0wmnpfu9dyfhezenjl4el4xwgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc3gkxw" />
    <content type="html">
      plan-dist-manifest.json&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:03:44Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxtlzv323huglc3f0c6zwzzx3307hwfl00a98uaq6fen9vs6xswygzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4ezsym</id>
    
      <title type="html"># This file contains NIP-19 `naddr` entities for repositories ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxtlzv323huglc3f0c6zwzzx3307hwfl00a98uaq6fen9vs6xswygzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4ezsym" />
    <content type="html">
      # This file contains NIP-19 `naddr` entities for repositories that accept this&lt;br/&gt;# project&amp;#39;s issues and patches.&lt;br/&gt;#&lt;br/&gt;# The file acts as a **read-only reference** for retrieving repository relays&lt;br/&gt;# when embedded in an `naddr` and mentions those repositories when opening&lt;br/&gt;# patches or issues. Modifications here will not affect in the relays, as the&lt;br/&gt;# file is **explicitly untracked**. Its goal is to simplify contributions by&lt;br/&gt;# removing the need for manual address entry.&lt;br/&gt;#&lt;br/&gt;# Each entry must start with &amp;#34;naddr&amp;#34;. Embedded relays are **strongly recommended**&lt;br/&gt;# to assist client-side discovery.&lt;br/&gt;#&lt;br/&gt;# Empty lines are ignored. Lines starting with &amp;#34;#&amp;#34; are treated as comments.&lt;br/&gt;&lt;br/&gt;naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpp4mhxue69uhkummn9ekx7mqppamhxue69uhkummnw3ezumt0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qg5waehxw309ahx7um5wghx77r5wghxgetkxpx8xj&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:03:41Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs22jq6adx6wkvalc87l5zkvdvsvc87lh87y4zzcvjvjzxprvutl8qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc9uc43</id>
    
      <title type="html">name: Rust on: push: branches: [ &amp;#34;*&amp;#34; ] pull_request: ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs22jq6adx6wkvalc87l5zkvdvsvc87lh87y4zzcvjvjzxprvutl8qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvc9uc43" />
    <content type="html">
      name: Rust&lt;br/&gt;&lt;br/&gt;on:&lt;br/&gt;  push:&lt;br/&gt;    branches: [ &amp;#34;*&amp;#34; ]&lt;br/&gt;  pull_request:&lt;br/&gt;    branches: [ &amp;#34;*&amp;#34; ]&lt;br/&gt;&lt;br/&gt;env:&lt;br/&gt;  CARGO_TERM_COLOR: always&lt;br/&gt;  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true&lt;br/&gt;  RUST_LOG: info&lt;br/&gt;&lt;br/&gt;jobs:&lt;br/&gt;  build:&lt;br/&gt;&lt;br/&gt;    runs-on: ${{ matrix.os }}&lt;br/&gt;    strategy:&lt;br/&gt;      matrix:&lt;br/&gt;        os: [ubuntu-latest, macos-15-intel, macos-latest, windows-latest]&lt;br/&gt;        features_args: [&amp;#34;&amp;#34;, &amp;#34;--no-default-features&amp;#34;, &amp;#34;--features nostr&amp;#34;]&lt;br/&gt;&lt;br/&gt;    steps:&lt;br/&gt;    - uses: actions/checkout@v4&lt;br/&gt;    - name: Install system deps (dbus)&lt;br/&gt;      if: runner.os == &amp;#39;Linux&amp;#39;&lt;br/&gt;      shell: bash&lt;br/&gt;      run: |&lt;br/&gt;        sudo apt-get update&lt;br/&gt;        sudo apt-get install -y pkg-config libdbus-1-dev&lt;br/&gt;    - name: Install protobuf (protoc)&lt;br/&gt;      shell: bash&lt;br/&gt;      run: |&lt;br/&gt;        set -euxo pipefail&lt;br/&gt;        if [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;macOS&amp;#34; ]]; then&lt;br/&gt;          brew update&lt;br/&gt;          brew install protobuf&lt;br/&gt;        elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Linux&amp;#34; ]]; then&lt;br/&gt;          sudo apt-get update&lt;br/&gt;          sudo apt-get install -y protobuf-compiler&lt;br/&gt;        elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Windows&amp;#34; ]]; then&lt;br/&gt;          choco install -y protoc&lt;br/&gt;        fi&lt;br/&gt;    - name: Build ${{ matrix.features_args }}&lt;br/&gt;      run: cargo build --workspace --verbose ${{ matrix.features_args }}&lt;br/&gt;    - name: Run workspace tests ${{ matrix.features_args }}&lt;br/&gt;      run: |&lt;br/&gt;        cargo test --workspace ${{ matrix.features_args }} -- --test-threads 1&lt;br/&gt;    - name: Run get_file_hash_core tests ${{ matrix.features_args }}&lt;br/&gt;      shell: bash&lt;br/&gt;      run: |&lt;br/&gt;        if [[ &amp;#34;${{ matrix.features_args }}&amp;#34; == &amp;#34;--features nostr&amp;#34; ]]; then&lt;br/&gt;          cargo test -p get_file_hash_core ${{ matrix.features_args }} -- --test-threads 1 --nocapture&lt;br/&gt;        else&lt;br/&gt;          cargo test -p get_file_hash_core ${{ matrix.features_args }} -- --test-threads 1&lt;br/&gt;        fi&lt;br/&gt;    - name: Run get_file_hash tests ${{ matrix.features_args }}&lt;br/&gt;      shell: bash&lt;br/&gt;      run: |&lt;br/&gt;        if [[ &amp;#34;${{ matrix.features_args }}&amp;#34; == &amp;#34;--features nostr&amp;#34; ]]; then&lt;br/&gt;          cargo test -p get_file_hash ${{ matrix.features_args }} -- --test-threads 1 --nocapture&lt;br/&gt;        else&lt;br/&gt;          cargo test -p get_file_hash ${{ matrix.features_args }} -- --test-threads 1&lt;br/&gt;        fi&lt;br/&gt;    - name: Build Release ${{ matrix.features_args }}&lt;br/&gt;      run: cargo build --workspace --release ${{ matrix.features_args }}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:03:30Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsrz0799zasg9yrvx49xpwhs8cajsj0y0zvwm08wgvwxlavngxumhszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4ptwj4</id>
    
      <title type="html">{ inputs = { nixpkgs.url = ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsrz0799zasg9yrvx49xpwhs8cajsj0y0zvwm08wgvwxlavngxumhszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4ptwj4" />
    <content type="html">
      {&lt;br/&gt;  inputs = {&lt;br/&gt;    nixpkgs.url = &amp;#34;github:NixOS/nixpkgs/nixos-unstable&amp;#34;;&lt;br/&gt;    rust-overlay.url = &amp;#34;github:oxalica/rust-overlay&amp;#34;;&lt;br/&gt;    flake-utils.url = &amp;#34;github:numtide/flake-utils&amp;#34;;&lt;br/&gt;  };&lt;br/&gt;&lt;br/&gt;  outputs =&lt;br/&gt;    {&lt;br/&gt;      nixpkgs,&lt;br/&gt;      rust-overlay,&lt;br/&gt;      flake-utils,&lt;br/&gt;      ...&lt;br/&gt;    }:&lt;br/&gt;    flake-utils.lib.eachDefaultSystem (&lt;br/&gt;      system:&lt;br/&gt;      let&lt;br/&gt;        overlays = [ (import rust-overlay) ];&lt;br/&gt;        pkgs = import nixpkgs { inherit system overlays; };&lt;br/&gt;      in&lt;br/&gt;      with pkgs;&lt;br/&gt;      {&lt;br/&gt;        devShells.default = mkShell {&lt;br/&gt;          packages = [&lt;br/&gt;            cargo-msrv&lt;br/&gt;            dbus&lt;br/&gt;            git-cliff&lt;br/&gt;            just&lt;br/&gt;            mdbook&lt;br/&gt;            nushell&lt;br/&gt;            pkg-config&lt;br/&gt;            taplo&lt;br/&gt;          ];&lt;br/&gt;&lt;br/&gt;          nativeBuildInputs = [&lt;br/&gt;            (lib.hiPrio rust-bin.nightly.&amp;#34;2025-08-07&amp;#34;.rustfmt)&lt;br/&gt;            rust-bin.stable.latest.default&lt;br/&gt;            rust-analyzer&lt;br/&gt;          ];&lt;br/&gt;        };&lt;br/&gt;&lt;br/&gt;        packages.default =&lt;br/&gt;          let&lt;br/&gt;            manifest = (pkgs.lib.importTOML ./Cargo.toml).package;&lt;br/&gt;          in&lt;br/&gt;          with pkgs;&lt;br/&gt;          rustPlatform.buildRustPackage {&lt;br/&gt;            pname = manifest.name;&lt;br/&gt;            version = manifest.version;&lt;br/&gt;            cargoLock.lockFile = ./Cargo.lock;&lt;br/&gt;            src = lib.cleanSource ./.;&lt;br/&gt;&lt;br/&gt;            nativeBuildInputs = [&lt;br/&gt;              pkg-config&lt;br/&gt;            ];&lt;br/&gt;&lt;br/&gt;            buildInputs = [&lt;br/&gt;              dbus&lt;br/&gt;            ];&lt;br/&gt;&lt;br/&gt;            meta = {&lt;br/&gt;              inherit (manifest) description homepage;&lt;br/&gt;              license = lib.licenses.gpl3Plus;&lt;br/&gt;            };&lt;br/&gt;          };&lt;br/&gt;      }&lt;br/&gt;    );&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:03:16Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxpgqx7mtj2qpaua82ez24l0ry8tme6chmnvypgzxd34s7s64ekxczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3p3yta</id>
    
      <title type="html"># This file was autogenerated by dist: ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxpgqx7mtj2qpaua82ez24l0ry8tme6chmnvypgzxd34s7s64ekxczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3p3yta" />
    <content type="html">
      # This file was autogenerated by dist: &lt;a href=&#34;https://axodotdev.github.io/cargo-dist&#34;&gt;https://axodotdev.github.io/cargo-dist&lt;/a&gt;&lt;br/&gt;#&lt;br/&gt;# Copyright 2022-2024, axodotdev&lt;br/&gt;# SPDX-License-Identifier: MIT or Apache-2.0&lt;br/&gt;#&lt;br/&gt;# CI that:&lt;br/&gt;#&lt;br/&gt;# * checks for a Git Tag that looks like a release&lt;br/&gt;# * builds artifacts with dist (archives, installers, hashes)&lt;br/&gt;# * uploads those artifacts to temporary workflow zip&lt;br/&gt;# * on success, uploads the artifacts to a GitHub Release&lt;br/&gt;#&lt;br/&gt;# Note that the GitHub Release will be created with a generated&lt;br/&gt;# title/body based on your changelogs.&lt;br/&gt;&lt;br/&gt;name: Release&lt;br/&gt;permissions:&lt;br/&gt;  &amp;#34;contents&amp;#34;: &amp;#34;write&amp;#34;&lt;br/&gt;&lt;br/&gt;# This task will run whenever you push a git tag that looks like a version&lt;br/&gt;# like &amp;#34;1.0.0&amp;#34;, &amp;#34;v0.1.0-prerelease.1&amp;#34;, &amp;#34;my-app/0.1.0&amp;#34;, &amp;#34;releases/v1.0.0&amp;#34;, etc.&lt;br/&gt;# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where&lt;br/&gt;# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION&lt;br/&gt;# must be a Cargo-style SemVer Version (must have at least major.minor.patch).&lt;br/&gt;#&lt;br/&gt;# If PACKAGE_NAME is specified, then the announcement will be for that&lt;br/&gt;# package (erroring out if it doesn&amp;#39;t have the given version or isn&amp;#39;t dist-able).&lt;br/&gt;#&lt;br/&gt;# If PACKAGE_NAME isn&amp;#39;t specified, then the announcement will be for all&lt;br/&gt;# (dist-able) packages in the workspace with that version (this mode is&lt;br/&gt;# intended for workspaces with only one dist-able package, or with all dist-able&lt;br/&gt;# packages versioned/released in lockstep).&lt;br/&gt;#&lt;br/&gt;# If you push multiple tags at once, separate instances of this workflow will&lt;br/&gt;# spin up, creating an independent announcement for each one. However, GitHub&lt;br/&gt;# will hard limit this to 3 tags per commit, as it will assume more tags is a&lt;br/&gt;# mistake.&lt;br/&gt;#&lt;br/&gt;# If there&amp;#39;s a prerelease-style suffix to the version, then the release(s)&lt;br/&gt;# will be marked as a prerelease.&lt;br/&gt;on:&lt;br/&gt;  pull_request:&lt;br/&gt;  push:&lt;br/&gt;    tags:&lt;br/&gt;      - &amp;#39;**[0-9]&#43;.[0-9]&#43;.[0-9]&#43;*&amp;#39;&lt;br/&gt;&lt;br/&gt;jobs:&lt;br/&gt;  # Run &amp;#39;dist plan&amp;#39; (or host) to determine what tasks we need to do&lt;br/&gt;  install-deps:&lt;br/&gt;    runs-on: &amp;#34;ubuntu-latest&amp;#34;&lt;br/&gt;    outputs:&lt;br/&gt;      val: ${{ steps.plan.outputs.manifest }}&lt;br/&gt;      tag: ${{ !github.event.pull_request &amp;amp;&amp;amp; github.ref_name || &amp;#39;&amp;#39; }}&lt;br/&gt;      tag-flag: ${{ !github.event.pull_request &amp;amp;&amp;amp; format(&amp;#39;--tag={0}&amp;#39;, github.ref_name) || &amp;#39;&amp;#39; }}&lt;br/&gt;      publishing: ${{ !github.event.pull_request }}&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;    steps:&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: false&lt;br/&gt;          submodules: recursive&lt;br/&gt;&lt;br/&gt;      - name: Install-deps&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          if [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Linux&amp;#34; ]]; then&lt;br/&gt;            sudo apt-get update&lt;br/&gt;            sudo apt-get install -y pkg-config libdbus-1-dev curl unzip&lt;br/&gt;&lt;br/&gt;            # Install a pinned modern protoc&lt;br/&gt;            PROTOC_ZIP=protoc-25.3-linux-x86_64.zip&lt;br/&gt;            curl -LO &lt;a href=&#34;https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP&#34;&gt;https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP&lt;/a&gt;&lt;br/&gt;            sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc&lt;br/&gt;            sudo unzip -o $PROTOC_ZIP -d /usr/local &amp;#39;include/*&amp;#39;&lt;br/&gt;            rm -f $PROTOC_ZIP&lt;br/&gt;&lt;br/&gt;          elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;macOS&amp;#34; ]]; then&lt;br/&gt;            brew install protobuf&lt;br/&gt;          elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Windows&amp;#34; ]]; then&lt;br/&gt;            choco install protoc --no-progress&lt;br/&gt;            echo &amp;#34;C:\ProgramData\chocolatey\bin&amp;#34; &amp;gt;&amp;gt; $GITHUB_PATH&lt;br/&gt;          fi&lt;br/&gt;&lt;br/&gt;          protoc --version&lt;br/&gt;&lt;br/&gt;  plan:&lt;br/&gt;    needs:&lt;br/&gt;      - install-deps&lt;br/&gt;    runs-on: &amp;#34;ubuntu-latest&amp;#34;&lt;br/&gt;    outputs:&lt;br/&gt;      val: ${{ steps.plan.outputs.manifest }}&lt;br/&gt;      tag: ${{ !github.event.pull_request &amp;amp;&amp;amp; github.ref_name || &amp;#39;&amp;#39; }}&lt;br/&gt;      tag-flag: ${{ !github.event.pull_request &amp;amp;&amp;amp; format(&amp;#39;--tag={0}&amp;#39;, github.ref_name) || &amp;#39;&amp;#39; }}&lt;br/&gt;      publishing: ${{ !github.event.pull_request }}&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;    steps:&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: false&lt;br/&gt;          submodules: recursive&lt;br/&gt;      - name: Install dist&lt;br/&gt;        # we specify bash to get pipefail; it guards against the `curl` command&lt;br/&gt;        # failing. otherwise `sh` won&amp;#39;t catch that `curl` returned non-0&lt;br/&gt;        shell: bash&lt;br/&gt;        run: &amp;#34;curl --proto &amp;#39;=https&amp;#39; --tlsv1.2 -LsSf &lt;a href=&#34;https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh&#34;&gt;https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh&lt;/a&gt; | sh&amp;#34;&lt;br/&gt;      - name: Cache dist&lt;br/&gt;        uses: actions/upload-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          name: cargo-dist-cache&lt;br/&gt;          path: ~/.cargo/bin/dist&lt;br/&gt;      # sure would be cool if github gave us proper conditionals...&lt;br/&gt;      # so here&amp;#39;s a doubly-nested ternary-via-truthiness to try to provide the best possible&lt;br/&gt;      # functionality based on whether this is a pull_request, and whether it&amp;#39;s from a fork.&lt;br/&gt;      # (PRs run on the *source* but secrets are usually on the *target* -- that&amp;#39;s *good*&lt;br/&gt;      # but also really annoying to build CI around when it needs secrets to work right.)&lt;br/&gt;      - id: plan&lt;br/&gt;        run: |&lt;br/&gt;          dist ${{ (!github.event.pull_request &amp;amp;&amp;amp; format(&amp;#39;host --steps=create --tag={0}&amp;#39;, github.ref_name)) || &amp;#39;plan&amp;#39; }} --output-format=json &amp;gt; plan-dist-manifest.json&lt;br/&gt;          echo &amp;#34;dist ran successfully&amp;#34;&lt;br/&gt;          cat plan-dist-manifest.json&lt;br/&gt;          echo &amp;#34;manifest=$(jq -c &amp;#34;.&amp;#34; plan-dist-manifest.json)&amp;#34; &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;      - name: &amp;#34;Upload dist-manifest.json&amp;#34;&lt;br/&gt;        uses: actions/upload-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          name: artifacts-plan-dist-manifest&lt;br/&gt;          path: plan-dist-manifest.json&lt;br/&gt;&lt;br/&gt;  # Build and packages all the platform-specific things&lt;br/&gt;  build-local-artifacts:&lt;br/&gt;    name: build-local-artifacts (${{ join(matrix.targets, &amp;#39;, &amp;#39;) }})&lt;br/&gt;    # Let the initial task tell us to not run (currently very blunt)&lt;br/&gt;    needs:&lt;br/&gt;      - plan&lt;br/&gt;      - install-deps&lt;br/&gt;    if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null &amp;amp;&amp;amp; (needs.plan.outputs.publishing == &amp;#39;true&amp;#39; || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == &amp;#39;upload&amp;#39;) }}&lt;br/&gt;    strategy:&lt;br/&gt;      fail-fast: false&lt;br/&gt;      # Target platforms/runners are computed by dist in create-release.&lt;br/&gt;      # Each member of the matrix has the following arguments:&lt;br/&gt;      #&lt;br/&gt;      # - runner: the github runner&lt;br/&gt;      # - dist-args: cli flags to pass to dist&lt;br/&gt;      # - install-dist: expression to run to install dist on the runner&lt;br/&gt;      #&lt;br/&gt;      # Typically there will be:&lt;br/&gt;      # - 1 &amp;#34;global&amp;#34; task that builds universal installers&lt;br/&gt;      # - N &amp;#34;local&amp;#34; tasks that build each platform&amp;#39;s binaries and platform-specific installers&lt;br/&gt;      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}&lt;br/&gt;    runs-on: ${{ matrix.runner }}&lt;br/&gt;    container: ${{ matrix.container &amp;amp;&amp;amp; matrix.container.image || null }}&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;      BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, &amp;#39;-&amp;#39;) }}-dist-manifest.json&lt;br/&gt;    steps:&lt;br/&gt;      - name: enable windows longpaths&lt;br/&gt;        run: |&lt;br/&gt;          git config --global core.longpaths true&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: false&lt;br/&gt;          submodules: recursive&lt;br/&gt;      - name: Install Rust non-interactively if not already installed&lt;br/&gt;        if: ${{ matrix.container }}&lt;br/&gt;        run: |&lt;br/&gt;          if ! command -v cargo &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then&lt;br/&gt;            curl --proto &amp;#39;=https&amp;#39; --tlsv1.2 -sSf &lt;a href=&#34;https://sh.rustup.rs&#34;&gt;https://sh.rustup.rs&lt;/a&gt; | sh -s -- -y&lt;br/&gt;            echo &amp;#34;$HOME/.cargo/bin&amp;#34; &amp;gt;&amp;gt; $GITHUB_PATH&lt;br/&gt;          fi&lt;br/&gt;      - name: Install dist&lt;br/&gt;        run: ${{ matrix.install_dist.run }}&lt;br/&gt;      # Get the dist-manifest&lt;br/&gt;      - name: Fetch local artifacts&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          pattern: artifacts-*&lt;br/&gt;          path: target/distrib/&lt;br/&gt;          merge-multiple: true&lt;br/&gt;      - name: Install dependencies&lt;br/&gt;        run: |&lt;br/&gt;          ${{ matrix.packages_install }}&lt;br/&gt;      - name: Install-deps&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          if [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Linux&amp;#34; ]]; then&lt;br/&gt;            sudo apt-get update&lt;br/&gt;            sudo apt-get install -y pkg-config libdbus-1-dev curl unzip&lt;br/&gt;&lt;br/&gt;            # Install a pinned modern protoc&lt;br/&gt;            PROTOC_ZIP=protoc-25.3-linux-x86_64.zip&lt;br/&gt;            curl -LO &lt;a href=&#34;https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP&#34;&gt;https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP&lt;/a&gt;&lt;br/&gt;            sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc&lt;br/&gt;            sudo unzip -o $PROTOC_ZIP -d /usr/local &amp;#39;include/*&amp;#39;&lt;br/&gt;            rm -f $PROTOC_ZIP&lt;br/&gt;&lt;br/&gt;          elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;macOS&amp;#34; ]]; then&lt;br/&gt;            brew install protobuf&lt;br/&gt;          elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Windows&amp;#34; ]]; then&lt;br/&gt;            choco install protoc --no-progress&lt;br/&gt;            echo &amp;#34;C:\ProgramData\chocolatey\bin&amp;#34; &amp;gt;&amp;gt; $GITHUB_PATH&lt;br/&gt;          fi&lt;br/&gt;&lt;br/&gt;          protoc --version&lt;br/&gt;&lt;br/&gt;      - name: Configure Cargo for Windows builds&lt;br/&gt;        if: runner.os == &amp;#39;Windows&amp;#39;&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          # Put target dir somewhere short and less likely to be locked&lt;br/&gt;          echo &amp;#34;CARGO_TARGET_DIR=D:/cargo-target&amp;#34; &amp;gt;&amp;gt; $GITHUB_ENV&lt;br/&gt;          # Reduce file-handle pressure / parallel writes&lt;br/&gt;          echo &amp;#34;CARGO_BUILD_JOBS=2&amp;#34; &amp;gt;&amp;gt; $GITHUB_ENV&lt;br/&gt;          # Avoid incremental artifacts (less churn, fewer locks)&lt;br/&gt;          echo &amp;#34;CARGO_INCREMENTAL=0&amp;#34; &amp;gt;&amp;gt; $GITHUB_ENV&lt;br/&gt;&lt;br/&gt;      - name: Build artifacts&lt;br/&gt;        run: |&lt;br/&gt;          # Actually do builds and make zips and whatnot&lt;br/&gt;          dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} &amp;gt; dist-manifest.json&lt;br/&gt;          echo &amp;#34;dist ran successfully&amp;#34;&lt;br/&gt;      - id: cargo-dist&lt;br/&gt;        name: Post-build&lt;br/&gt;        # We force bash here just because github makes it really hard to get values up&lt;br/&gt;        # to &amp;#34;real&amp;#34; actions without writing to env-vars, and writing to env-vars has&lt;br/&gt;        # inconsistent syntax between shell and powershell.&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          # Parse out what we just built and upload it to scratch storage&lt;br/&gt;          echo &amp;#34;paths&amp;lt;&amp;lt;EOF&amp;#34; &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;          dist print-upload-files-from-manifest --manifest dist-manifest.json &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;          echo &amp;#34;EOF&amp;#34; &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;&lt;br/&gt;          cp dist-manifest.json &amp;#34;$BUILD_MANIFEST_NAME&amp;#34;&lt;br/&gt;      - name: &amp;#34;Upload artifacts&amp;#34;&lt;br/&gt;        uses: actions/upload-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          name: artifacts-build-local-${{ join(matrix.targets, &amp;#39;_&amp;#39;) }}&lt;br/&gt;          path: |&lt;br/&gt;            ${{ steps.cargo-dist.outputs.paths }}&lt;br/&gt;            ${{ env.BUILD_MANIFEST_NAME }}&lt;br/&gt;&lt;br/&gt;  # Build and package all the platform-agnostic(ish) things&lt;br/&gt;  build-global-artifacts:&lt;br/&gt;    needs:&lt;br/&gt;      - plan&lt;br/&gt;      - install-deps&lt;br/&gt;      - build-local-artifacts&lt;br/&gt;    runs-on: &amp;#34;ubuntu-latest&amp;#34;&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;      BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json&lt;br/&gt;    steps:&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: false&lt;br/&gt;          submodules: recursive&lt;br/&gt;      - name: Install cached dist&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          name: cargo-dist-cache&lt;br/&gt;          path: ~/.cargo/bin/&lt;br/&gt;      - run: chmod &#43;x ~/.cargo/bin/dist&lt;br/&gt;      - name: Install-deps&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          if [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Linux&amp;#34; ]]; then&lt;br/&gt;            sudo apt-get update&lt;br/&gt;            sudo apt-get install -y pkg-config libdbus-1-dev curl unzip&lt;br/&gt;&lt;br/&gt;            # Install a pinned modern protoc&lt;br/&gt;            PROTOC_ZIP=protoc-25.3-linux-x86_64.zip&lt;br/&gt;            curl -LO &lt;a href=&#34;https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP&#34;&gt;https://github.com/protocolbuffers/protobuf/releases/download/v25.3/$PROTOC_ZIP&lt;/a&gt;&lt;br/&gt;            sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc&lt;br/&gt;            sudo unzip -o $PROTOC_ZIP -d /usr/local &amp;#39;include/*&amp;#39;&lt;br/&gt;            rm -f $PROTOC_ZIP&lt;br/&gt;&lt;br/&gt;          elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;macOS&amp;#34; ]]; then&lt;br/&gt;            brew install protobuf&lt;br/&gt;          elif [[ &amp;#34;${{ runner.os }}&amp;#34; == &amp;#34;Windows&amp;#34; ]]; then&lt;br/&gt;            choco install protoc --no-progress&lt;br/&gt;            echo &amp;#34;C:\ProgramData\chocolatey\bin&amp;#34; &amp;gt;&amp;gt; $GITHUB_PATH&lt;br/&gt;          fi&lt;br/&gt;&lt;br/&gt;          protoc --version&lt;br/&gt;&lt;br/&gt;      # Get all the local artifacts for the global tasks to use (for e.g. checksums)&lt;br/&gt;      - name: Fetch local artifacts&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          pattern: artifacts-*&lt;br/&gt;          path: target/distrib/&lt;br/&gt;          merge-multiple: true&lt;br/&gt;      - id: cargo-dist&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json &amp;#34;--artifacts=global&amp;#34; &amp;gt; dist-manifest.json&lt;br/&gt;          echo &amp;#34;dist ran successfully&amp;#34;&lt;br/&gt;&lt;br/&gt;          # Parse out what we just built and upload it to scratch storage&lt;br/&gt;          echo &amp;#34;paths&amp;lt;&amp;lt;EOF&amp;#34; &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;          jq --raw-output &amp;#34;.upload_files[]&amp;#34; dist-manifest.json &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;          echo &amp;#34;EOF&amp;#34; &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;&lt;br/&gt;          cp dist-manifest.json &amp;#34;$BUILD_MANIFEST_NAME&amp;#34;&lt;br/&gt;      - name: &amp;#34;Upload artifacts&amp;#34;&lt;br/&gt;        uses: actions/upload-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          name: artifacts-build-global&lt;br/&gt;          path: |&lt;br/&gt;            ${{ steps.cargo-dist.outputs.paths }}&lt;br/&gt;            ${{ env.BUILD_MANIFEST_NAME }}&lt;br/&gt;  # Determines if we should publish/announce&lt;br/&gt;  host:&lt;br/&gt;    needs:&lt;br/&gt;      - plan&lt;br/&gt;      - install-deps&lt;br/&gt;      - build-local-artifacts&lt;br/&gt;      - build-global-artifacts&lt;br/&gt;    # Only run if we&amp;#39;re &amp;#34;publishing&amp;#34;, and only if plan, local and global didn&amp;#39;t fail (skipped is fine)&lt;br/&gt;    if: ${{ always() &amp;amp;&amp;amp; needs.plan.result == &amp;#39;success&amp;#39; &amp;amp;&amp;amp; needs.plan.outputs.publishing == &amp;#39;true&amp;#39; &amp;amp;&amp;amp; (needs.build-global-artifacts.result == &amp;#39;skipped&amp;#39; || needs.build-global-artifacts.result == &amp;#39;success&amp;#39;) &amp;amp;&amp;amp; (needs.build-local-artifacts.result == &amp;#39;skipped&amp;#39; || needs.build-local-artifacts.result == &amp;#39;success&amp;#39;) }}&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;    runs-on: &amp;#34;ubuntu-latest&amp;#34;&lt;br/&gt;    outputs:&lt;br/&gt;      val: ${{ steps.host.outputs.manifest }}&lt;br/&gt;    steps:&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: false&lt;br/&gt;          submodules: recursive&lt;br/&gt;      - name: Install cached dist&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          name: cargo-dist-cache&lt;br/&gt;          path: ~/.cargo/bin/&lt;br/&gt;      - run: chmod &#43;x ~/.cargo/bin/dist&lt;br/&gt;      # Fetch artifacts from scratch-storage&lt;br/&gt;      - name: Fetch artifacts&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          pattern: artifacts-*&lt;br/&gt;          path: target/distrib/&lt;br/&gt;          merge-multiple: true&lt;br/&gt;      - id: host&lt;br/&gt;        shell: bash&lt;br/&gt;        run: |&lt;br/&gt;          dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json &amp;gt; dist-manifest.json&lt;br/&gt;          echo &amp;#34;artifacts uploaded and released successfully&amp;#34;&lt;br/&gt;          cat dist-manifest.json&lt;br/&gt;          echo &amp;#34;manifest=$(jq -c &amp;#34;.&amp;#34; dist-manifest.json)&amp;#34; &amp;gt;&amp;gt; &amp;#34;$GITHUB_OUTPUT&amp;#34;&lt;br/&gt;      - name: &amp;#34;Upload dist-manifest.json&amp;#34;&lt;br/&gt;        uses: actions/upload-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          # Overwrite the previous copy&lt;br/&gt;          name: artifacts-dist-manifest&lt;br/&gt;          path: dist-manifest.json&lt;br/&gt;      # Create a GitHub Release while uploading all files to it&lt;br/&gt;      - name: &amp;#34;Download GitHub Artifacts&amp;#34;&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          pattern: artifacts-*&lt;br/&gt;          path: artifacts&lt;br/&gt;          merge-multiple: true&lt;br/&gt;      - name: Cleanup&lt;br/&gt;        run: |&lt;br/&gt;          # Remove the granular manifests&lt;br/&gt;          rm -f artifacts/*-dist-manifest.json&lt;br/&gt;      - name: Create GitHub Release&lt;br/&gt;        env:&lt;br/&gt;          PRERELEASE_FLAG: &amp;#34;${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease &amp;amp;&amp;amp; &amp;#39;--prerelease&amp;#39; || &amp;#39;&amp;#39; }}&amp;#34;&lt;br/&gt;          ANNOUNCEMENT_TITLE: &amp;#34;${{ fromJson(steps.host.outputs.manifest).announcement_title }}&amp;#34;&lt;br/&gt;          ANNOUNCEMENT_BODY: &amp;#34;${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}&amp;#34;&lt;br/&gt;          RELEASE_COMMIT: &amp;#34;${{ github.sha }}&amp;#34;&lt;br/&gt;        run: |&lt;br/&gt;          # Write and read notes from a file to avoid quoting breaking things&lt;br/&gt;          echo &amp;#34;$ANNOUNCEMENT_BODY&amp;#34; &amp;gt; $RUNNER_TEMP/notes.txt&lt;br/&gt;&lt;br/&gt;          gh release create &amp;#34;${{ needs.plan.outputs.tag }}&amp;#34; --target &amp;#34;$RELEASE_COMMIT&amp;#34; $PRERELEASE_FLAG --title &amp;#34;$ANNOUNCEMENT_TITLE&amp;#34; --notes-file &amp;#34;$RUNNER_TEMP/notes.txt&amp;#34; artifacts/*&lt;br/&gt;&lt;br/&gt;  publish-homebrew-formula:&lt;br/&gt;    needs:&lt;br/&gt;      - plan&lt;br/&gt;      - host&lt;br/&gt;      - install-deps&lt;br/&gt;      - build-local-artifacts&lt;br/&gt;      - build-global-artifacts&lt;br/&gt;    runs-on: &amp;#34;ubuntu-latest&amp;#34;&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;      PLAN: ${{ needs.plan.outputs.val }}&lt;br/&gt;      GITHUB_USER: &amp;#34;axo bot&amp;#34;&lt;br/&gt;      GITHUB_EMAIL: &amp;#34;admin&#43;bot@axo.dev&amp;#34;&lt;br/&gt;    if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}&lt;br/&gt;    steps:&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: true&lt;br/&gt;          repository: &amp;#34;gnostr-org/homebrew-gnostr&amp;#34;&lt;br/&gt;          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}&lt;br/&gt;      # So we have access to the formula&lt;br/&gt;      - name: Fetch homebrew formulae&lt;br/&gt;        uses: actions/download-artifact@v4&lt;br/&gt;        with:&lt;br/&gt;          pattern: artifacts-*&lt;br/&gt;          path: Formula/&lt;br/&gt;          merge-multiple: true&lt;br/&gt;      # This is extra complex because you can make your Formula name not match your app name&lt;br/&gt;      # so we need to find releases with a *.rb file, and publish with that filename.&lt;br/&gt;      - name: Commit formula files&lt;br/&gt;        run: |&lt;br/&gt;          git config --global user.name &amp;#34;${GITHUB_USER}&amp;#34;&lt;br/&gt;          git config --global user.email &amp;#34;${GITHUB_EMAIL}&amp;#34;&lt;br/&gt;&lt;br/&gt;          for release in $(echo &amp;#34;$PLAN&amp;#34; | jq --compact-output &amp;#39;.releases[] | select([.artifacts[] | endswith(&amp;#34;.rb&amp;#34;)] | any)&amp;#39;); do&lt;br/&gt;            filename=$(echo &amp;#34;$release&amp;#34; | jq &amp;#39;.artifacts[] | select(endswith(&amp;#34;.rb&amp;#34;))&amp;#39; --raw-output)&lt;br/&gt;            name=$(echo &amp;#34;$filename&amp;#34; | sed &amp;#34;s/\.rb$//&amp;#34;)&lt;br/&gt;            version=$(echo &amp;#34;$release&amp;#34; | jq .app_version --raw-output)&lt;br/&gt;&lt;br/&gt;            export PATH=&amp;#34;/home/linuxbrew/.linuxbrew/bin:$PATH&amp;#34;&lt;br/&gt;            brew update&lt;br/&gt;            # We avoid reformatting user-provided data such as the app description and homepage.&lt;br/&gt;            brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix &amp;#34;Formula/${filename}&amp;#34; || true&lt;br/&gt;&lt;br/&gt;            git add &amp;#34;Formula/${filename}&amp;#34;&lt;br/&gt;            git commit -m &amp;#34;${name} ${version}&amp;#34;&lt;br/&gt;          done&lt;br/&gt;          git push&lt;br/&gt;&lt;br/&gt;  announce:&lt;br/&gt;    needs:&lt;br/&gt;      - plan&lt;br/&gt;      - install-deps&lt;br/&gt;      - build-local-artifacts&lt;br/&gt;      - build-global-artifacts&lt;br/&gt;      - host&lt;br/&gt;      - publish-homebrew-formula&lt;br/&gt;    # use &amp;#34;always() &amp;amp;&amp;amp; ...&amp;#34; to allow us to wait for all publish jobs while&lt;br/&gt;    # still allowing individual publish jobs to skip themselves (for prereleases).&lt;br/&gt;    # &amp;#34;host&amp;#34; however must run to completion, no skipping allowed!&lt;br/&gt;    if: ${{ always() &amp;amp;&amp;amp; needs.host.result == &amp;#39;success&amp;#39; &amp;amp;&amp;amp; (needs.publish-homebrew-formula.result == &amp;#39;skipped&amp;#39; || needs.publish-homebrew-formula.result == &amp;#39;success&amp;#39;) }}&lt;br/&gt;    runs-on: &amp;#34;ubuntu-latest&amp;#34;&lt;br/&gt;    env:&lt;br/&gt;      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;br/&gt;    steps:&lt;br/&gt;      - uses: actions/checkout@v4&lt;br/&gt;        with:&lt;br/&gt;          persist-credentials: false&lt;br/&gt;          submodules: recursive&lt;br/&gt;
    </content>
    <updated>2026-04-05T05:03:09Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxjdghkaqyhvq23uun582eclzr5d4e28f5dulzp93gz92j8gpcfeszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhql5kd</id>
    
      <title type="html"># View an Issue By ID &amp;gt; `n34 issue view` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxjdghkaqyhvq23uun582eclzr5d4e28f5dulzp93gz92j8gpcfeszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvhql5kd" />
    <content type="html">
      # View an Issue By ID&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue view` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;View an issue by its ID&lt;br/&gt;&lt;br/&gt;Usage: n34 issue view [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The issue id to view it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Simply provide the issue ID in `note` or `nevent` format to retrieve and display&lt;br/&gt;the issue details.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:59:23Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs2yras42h5x4s223qap9m0jkjn8xp4e2eu3zp55yfdrxppmfl47mgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0ufmh8</id>
    
      <title type="html"># Resolves an Issue &amp;gt; `n34 issue resolve` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs2yras42h5x4s223qap9m0jkjn8xp4e2eu3zp55yfdrxppmfl47mgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0ufmh8" />
    <content type="html">
      # Resolves an Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue resolve` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Resolves an issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue resolve [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The issue id to resolve it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1631` (Resolved status) event for the specified issue.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:59:11Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsdkaujmxcdw7gj7a6k9vqs2su8rj3747eqj3nduue4lv9q6r56aaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv75yef7</id>
    
      <title type="html"># Reopen a Closed Issue &amp;gt; `n34 issue reopen` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsdkaujmxcdw7gj7a6k9vqs2su8rj3747eqj3nduue4lv9q6r56aaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv75yef7" />
    <content type="html">
      # Reopen a Closed Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue reopen` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Reopens a closed issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue reopen [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The ID of the closed issue to reopen&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1630` (Open status) for the specified issue. The issue have to&lt;br/&gt;be closed.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:59:00Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsfz7lm5hg40ft8wajexuzv43dl2ek9zvw08293ju2p3dm3rac7rgczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvm5r647</id>
    
      <title type="html"># Create an Issue &amp;gt; `n34 issue new` Command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsfz7lm5hg40ft8wajexuzv43dl2ek9zvw08293ju2p3dm3rac7rgczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvm5r647" />
    <content type="html">
      # Create an Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue new` Command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Create a new repository issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue new [OPTIONS] &amp;lt;--content &amp;lt;CONTENT&amp;gt;|--editor&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -c, --content &amp;lt;CONTENT&amp;gt;          Markdown content for the issue. Cannot be used together with the `--editor` flag&lt;br/&gt;  -e, --editor                     Opens the user&amp;#39;s default editor to write issue content. The first line will be used as the issue subject&lt;br/&gt;      --subject &amp;lt;SUBJECT&amp;gt;          The issue subject. Cannot be used together with the `--editor` flag&lt;br/&gt;  -l, --label &amp;lt;LABEL&amp;gt;              Labels for the issue. Can be specified as arguments (-l bug) or hashtags in content (#bug)&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Use the `n34 issue new` command to create a new issue in a repository. This&lt;br/&gt;command supports the [NIP-21] (`nostr:` URI scheme) and hashtags within the&lt;br/&gt;issue content. When you mention public keys in the content, they will be&lt;br/&gt;included in the event tags. Additionally, using hashtags like `#bug` in the&lt;br/&gt;issue body will automatically apply them as labels.&lt;br/&gt;&lt;br/&gt;You must choose between the `--content` and `--editor` options. With&lt;br/&gt;`--content`, you provide the issue content directly in the command. With&lt;br/&gt;`--editor`, your default `$EDITOR` will open, allowing you to write the issue&lt;br/&gt;content. The first line of the editor&amp;#39;s output will be used as the issue&lt;br/&gt;subject.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:58:49Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs9nnsq74480tge2tpu8ptjphdnqsu98ptnqamswh8rkeg8qxq6ycgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvg6dpj6</id>
    
      <title type="html"># Draft an Open Patch &amp;gt; `n34 patch draft` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs9nnsq74480tge2tpu8ptjphdnqsu98ptnqamswh8rkeg8qxq6ycgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvg6dpj6" />
    <content type="html">
      # Draft an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch draft` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Converts an open patch to draft state&lt;br/&gt;&lt;br/&gt;Usage: n34 patch draft [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The open patch id to draft it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1633` (Draft status) for the specified patch. The patch have to&lt;br/&gt;be open.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:41Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs26zf4a7palp6ashjf6nw363yrrqmy7mlt3xu6rkkyedfk7l2a96qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv87wh46</id>
    
      <title type="html">use frost_secp256k1_tr as frost; use frost::{Identifier, ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs26zf4a7palp6ashjf6nw363yrrqmy7mlt3xu6rkkyedfk7l2a96qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv87wh46" />
    <content type="html">
      use frost_secp256k1_tr as frost;&lt;br/&gt;use frost::{Identifier, keys::IdentifierList, round1, round2};&lt;br/&gt;use rand_chacha::ChaCha20Rng;&lt;br/&gt;use rand_chacha::rand_core::SeedableRng;&lt;br/&gt;use std::fs;&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    // 1. SETUP: Initial Key Generation (The &amp;#34;Genesis&amp;#34; event)&lt;br/&gt;    let mut dealer_rng = ChaCha20Rng::from_seed([0u8; 32]);&lt;br/&gt;    let min_signers = 2;&lt;br/&gt;    let (shares, pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        3, min_signers, IdentifierList::Default, &amp;amp;mut dealer_rng&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    // 2. PERSISTENCE: Save Participant 1&amp;#39;s KeyPackage to a file&lt;br/&gt;    let p1_id = Identifier::try_from(1u16)?;&lt;br/&gt;    let p1_key_pkg = frost::keys::KeyPackage::new(&lt;br/&gt;        p1_id,&lt;br/&gt;        *shares[&amp;amp;p1_id].signing_share(),&lt;br/&gt;        frost::keys::VerifyingShare::from(*shares[&amp;amp;p1_id].signing_share()),&lt;br/&gt;        *pubkey_package.verifying_key(),&lt;br/&gt;        min_signers,&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    // Serialize to JSON (standard for many Nostr/Git tools)&lt;br/&gt;    let p1_json = serde_json::to_string_pretty(&amp;amp;p1_key_pkg)?;&lt;br/&gt;    fs::write(&amp;#34;p1_key.json&amp;#34;, p1_json)?;&lt;br/&gt;    &lt;br/&gt;    let pub_json = serde_json::to_string_pretty(&amp;amp;pubkey_package)?;&lt;br/&gt;    fs::write(&amp;#34;group_public.json&amp;#34;, pub_json)?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;--- BIP-64MOD: Key Persistence ---&amp;#34;);&lt;br/&gt;    println!(&amp;#34;✅ Saved p1_key.json and group_public.json to disk.&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // 3. RELOAD: Simulate a Signer waking up later&lt;br/&gt;    let p1_loaded_json = fs::read_to_string(&amp;#34;p1_key.json&amp;#34;)?;&lt;br/&gt;    let p1_reloaded_pkg: frost::keys::KeyPackage = serde_json::from_str(&amp;amp;p1_loaded_json)?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;✅ Reloaded KeyPackage for Participant: {:?}&amp;#34;, p1_reloaded_pkg.identifier());&lt;br/&gt;&lt;br/&gt;    // 4. SIGN: Use the reloaded key to sign a new Git Commit Hash&lt;br/&gt;    let mut rng = ChaCha20Rng::from_seed([100u8; 32]); // Fresh seed for this specific signing session&lt;br/&gt;    let (nonces, commitments) = round1::commit(p1_reloaded_pkg.signing_share(), &amp;amp;mut rng);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;\nGenerated Nonce for new session:&amp;#34;);&lt;br/&gt;    println!(&amp;#34;  Commitment: {}&amp;#34;, hex::encode(commitments.serialize()?));&lt;br/&gt;&lt;br/&gt;    // Cleanup files for the example&lt;br/&gt;    // fs::remove_file(&amp;#34;p1_key.json&amp;#34;)?;&lt;br/&gt;    // fs::remove_file(&amp;#34;group_public.json&amp;#34;)?;&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(not(feature = &amp;#34;nostr&amp;#34;))]&lt;br/&gt;fn main() { println!(&amp;#34;Enable nostr feature.&amp;#34;); }&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:34Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsv4uxa295nzkj2vzyjd95r8keram4fju48ruuraq0h88v0vacmuzqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0pw557</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsv4uxa295nzkj2vzyjd95r8keram4fju48ruuraq0h88v0vacmuzqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv0pw557" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::Args;&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;pub struct PowArgs {&lt;br/&gt;    /// The new default PoW difficulty&lt;br/&gt;    difficulty: u8,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for PowArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        options.config.pow = Some(self.difficulty);&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:33Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsx3ss8vx7cauvcsw6u0v3z36lrjq695pzgeydhq8vt2xs046g0xsqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2nqrdw</id>
    
      <title type="html"># Reopens a Closed or Drafted Patch &amp;gt; `n34 patch reopen` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsx3ss8vx7cauvcsw6u0v3z36lrjq695pzgeydhq8vt2xs046g0xsqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2nqrdw" />
    <content type="html">
      # Reopens a Closed or Drafted Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch reopen` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Reopens a closed or drafted patch&lt;br/&gt;&lt;br/&gt;Usage: n34 patch reopen [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The closed/drafted patch id to reopen it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified patch. The patch have to&lt;br/&gt;be closed or drafted.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:33Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsx84jcc9df3jn653vq542mprvdg9rz2mhaaga9sn4r65zk3kuaysgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvejxfwj</id>
    
      <title type="html"># Issue Management Using `n34`, you can manage Git issues stored ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsx84jcc9df3jn653vq542mprvdg9rz2mhaaga9sn4r65zk3kuaysgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvejxfwj" />
    <content type="html">
      # Issue Management&lt;br/&gt;&lt;br/&gt;Using `n34`, you can manage Git issues stored in Nostr relays, adhering to&lt;br/&gt;the [NIP-34] standard. In Nostr, events are immutable, meaning their IDs are&lt;br/&gt;derived from the SHA-256 hash of their timestamp, content, author, and tags.&lt;br/&gt;As a result, issues cannot be edited directly. However, with `n34`, you can&lt;br/&gt;create new issues, view existing ones, or update their status—such as closing,&lt;br/&gt;resolving, or reopening them.&lt;br/&gt;&lt;br/&gt;[NIP-34] introduces support for drafting issues, though this feature is not&lt;br/&gt;currently implemented in `n34` due to the lack of a clear use case for drafting&lt;br/&gt;issues. The inclusion of this functionality may stem from its shared use in both&lt;br/&gt;issues and patches, suggesting it was primarily designed for patch management.&lt;br/&gt;&lt;br/&gt;[NIP-34]: &lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&lt;/a&gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:33Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsyaftv5mwytkgythd4r35fscu0pvcf2vdt8rs0j59d8q0qgxa3cdgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvla74tu</id>
    
      <title type="html"># Reopen a Closed Issue &amp;gt; `n34 issue reopen` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsyaftv5mwytkgythd4r35fscu0pvcf2vdt8rs0j59d8q0qgxa3cdgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvla74tu" />
    <content type="html">
      # Reopen a Closed Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue reopen` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Reopens a closed issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue reopen [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The ID of the closed issue to reopen&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1630` (Open status) for the specified issue. The issue have to&lt;br/&gt;be closed.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:33Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqszxzejevaydtrfalkhnmsy7c8pcwutmra95apl9679qmpy09d5phczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdakz2a</id>
    
      <title type="html"># Closes an Open or Drafted Patch &amp;gt; `n34 patch close` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqszxzejevaydtrfalkhnmsy7c8pcwutmra95apl9679qmpy09d5phczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdakz2a" />
    <content type="html">
      # Closes an Open or Drafted Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch close` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Closes an open or drafted patch&lt;br/&gt;&lt;br/&gt;Usage: n34 patch close [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The open/drafted patch id to close it. Must be orignal root patch&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified patch. The patch have to&lt;br/&gt;be open or drafted.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:30Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqspszgxuvt4dwl5crdxu4vnkt35ejssjxmykqr2sag5s8jfp5cam6gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsq2kdh</id>
    
      <title type="html">use frost_secp256k1_tr as frost; use frost::{Identifier, ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqspszgxuvt4dwl5crdxu4vnkt35ejssjxmykqr2sag5s8jfp5cam6gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvsq2kdh" />
    <content type="html">
      use frost_secp256k1_tr as frost;&lt;br/&gt;use frost::{Identifier, keys::IdentifierList, round1, round2};&lt;br/&gt;use rand_chacha::ChaCha20Rng;&lt;br/&gt;use rand_chacha::rand_core::SeedableRng;&lt;br/&gt;use std::collections::BTreeMap;&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    // 1. RECREATE CONTEXT (Same as Signer)&lt;br/&gt;    let mut dealer_rng = ChaCha20Rng::from_seed([0u8; 32]);&lt;br/&gt;    let min_signers = 2;&lt;br/&gt;    let (shares, pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        3, min_signers, IdentifierList::Default, &amp;amp;mut dealer_rng&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    let p1_id = Identifier::try_from(1u16)?;&lt;br/&gt;    let p2_id = Identifier::try_from(2u16)?;&lt;br/&gt;&lt;br/&gt;    // 2. SIMULATE SIGNING (Round 1 &amp;amp; 2)&lt;br/&gt;    let mut rng1 = ChaCha20Rng::from_seed([1u8; 32]);&lt;br/&gt;    let (p1_nonces, p1_commitments) = round1::commit(shares[&amp;amp;p1_id].signing_share(), &amp;amp;mut rng1);&lt;br/&gt;    let mut rng2 = ChaCha20Rng::from_seed([2u8; 32]);&lt;br/&gt;    let (p2_nonces, p2_commitments) = round1::commit(shares[&amp;amp;p2_id].signing_share(), &amp;amp;mut rng2);&lt;br/&gt;&lt;br/&gt;    let message = b&amp;#34;gnostr-gcc-verification-test&amp;#34;;&lt;br/&gt;    let mut commitments_map = BTreeMap::new();&lt;br/&gt;    commitments_map.insert(p1_id, p1_commitments);&lt;br/&gt;    commitments_map.insert(p2_id, p2_commitments);&lt;br/&gt;    let signing_package = frost::SigningPackage::new(commitments_map, message);&lt;br/&gt;&lt;br/&gt;    // Generate shares (using the KeyPackage method we perfected)&lt;br/&gt;    let p1_key_pkg = frost::keys::KeyPackage::new(p1_id, *shares[&amp;amp;p1_id].signing_share(), &lt;br/&gt;        frost::keys::VerifyingShare::from(*shares[&amp;amp;p1_id].signing_share()), &lt;br/&gt;        *pubkey_package.verifying_key(), min_signers);&lt;br/&gt;    let p2_key_pkg = frost::keys::KeyPackage::new(p2_id, *shares[&amp;amp;p2_id].signing_share(), &lt;br/&gt;        frost::keys::VerifyingShare::from(*shares[&amp;amp;p2_id].signing_share()), &lt;br/&gt;        *pubkey_package.verifying_key(), min_signers);&lt;br/&gt;&lt;br/&gt;    let p1_sig_share = round2::sign(&amp;amp;signing_package, &amp;amp;p1_nonces, &amp;amp;p1_key_pkg)?;&lt;br/&gt;    let p2_sig_share = round2::sign(&amp;amp;signing_package, &amp;amp;p2_nonces, &amp;amp;p2_key_pkg)?;&lt;br/&gt;&lt;br/&gt;    // 3. COORDINATOR: AGGREGATION&lt;br/&gt;    println!(&amp;#34;--- BIP-64MOD: Coordinator Aggregation ---&amp;#34;);&lt;br/&gt;    &lt;br/&gt;    let mut shares_map = BTreeMap::new();&lt;br/&gt;    shares_map.insert(p1_id, p1_sig_share);&lt;br/&gt;    shares_map.insert(p2_id, p2_sig_share);&lt;br/&gt;&lt;br/&gt;    let final_signature = frost::aggregate(&lt;br/&gt;        &amp;amp;signing_package, &lt;br/&gt;        &amp;amp;shares_map, &lt;br/&gt;        &amp;amp;pubkey_package&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    let sig_bytes = final_signature.serialize()?;&lt;br/&gt;    println!(&amp;#34;✅ Aggregation Successful!&amp;#34;);&lt;br/&gt;    println!(&amp;#34;Final Signature (Hex): {}&amp;#34;, hex::encode(&amp;amp;sig_bytes));&lt;br/&gt;&lt;br/&gt;    // 4. VERIFICATION (The moment of truth)&lt;br/&gt;    pubkey_package.verifying_key().verify(message, &amp;amp;final_signature)?;&lt;br/&gt;    println!(&amp;#34;🛡️  Signature Verified against Group Public Key!&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(not(feature = &amp;#34;nostr&amp;#34;))]&lt;br/&gt;fn main() { println!(&amp;#34;Enable nostr feature.&amp;#34;); }&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:23Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqstwytdnxda58vrnfzavcr4vpfcd0ehvxy4dkrxdtudzl9yxyz4rqczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2ha6r7</id>
    
      <title type="html"># Create an Issue &amp;gt; `n34 issue new` Command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqstwytdnxda58vrnfzavcr4vpfcd0ehvxy4dkrxdtudzl9yxyz4rqczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv2ha6r7" />
    <content type="html">
      # Create an Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue new` Command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Create a new repository issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue new [OPTIONS] &amp;lt;--content &amp;lt;CONTENT&amp;gt;|--editor&amp;gt;&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -c, --content &amp;lt;CONTENT&amp;gt;          Markdown content for the issue. Cannot be used together with the `--editor` flag&lt;br/&gt;  -e, --editor                     Opens the user&amp;#39;s default editor to write issue content. The first line will be used as the issue subject&lt;br/&gt;      --subject &amp;lt;SUBJECT&amp;gt;          The issue subject. Cannot be used together with the `--editor` flag&lt;br/&gt;  -l, --label &amp;lt;LABEL&amp;gt;              Labels for the issue. Can be specified as arguments (-l bug) or hashtags in content (#bug)&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Use the `n34 issue new` command to create a new issue in a repository. This&lt;br/&gt;command supports the [NIP-21] (`nostr:` URI scheme) and hashtags within the&lt;br/&gt;issue content. When you mention public keys in the content, they will be&lt;br/&gt;included in the event tags. Additionally, using hashtags like `#bug` in the&lt;br/&gt;issue body will automatically apply them as labels.&lt;br/&gt;&lt;br/&gt;You must choose between the `--content` and `--editor` options. With&lt;br/&gt;`--content`, you provide the issue content directly in the command. With&lt;br/&gt;`--editor`, your default `$EDITOR` will open, allowing you to write the issue&lt;br/&gt;content. The first line of the editor&amp;#39;s output will be used as the issue&lt;br/&gt;subject.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:22Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs2wch7fvk4qglhez024clfzha5z9vaw75zcf9wfexd74wpy965a4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvatwzqh</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs2wch7fvk4qglhez024clfzha5z9vaw75zcf9wfexd74wpy965a4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvatwzqh" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use std::net::SocketAddr;&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{CliOptions, options_state::DEFAULT_NIP07_PROXY_ADDR, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;options&amp;#34;)&lt;br/&gt;            .required(true)&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct Nip07Args {&lt;br/&gt;    /// Enable NIP-07 as the default signer.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    enable:  bool,&lt;br/&gt;    /// Disable NIP-07 as the default signer.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;, group = &amp;#34;disable_options&amp;#34;)]&lt;br/&gt;    disable: bool,&lt;br/&gt;    /// Set the default `ip:port` for the browser signer proxy.&lt;br/&gt;    #[arg(long, group = &amp;#34;disable_options&amp;#34;)]&lt;br/&gt;    addr:    Option&amp;lt;SocketAddr&amp;gt;,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for Nip07Args {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        if self.enable {&lt;br/&gt;            let addr = self.addr.unwrap_or(DEFAULT_NIP07_PROXY_ADDR);&lt;br/&gt;            options.config.nip07 = Some(addr)&lt;br/&gt;        } else {&lt;br/&gt;            options.config.nip07 = None&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:22Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsgefrql6vwgf0z4q64dym7fnuq02j34cpwul3z9f4fccfdxptvgggzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvahnga</id>
    
      <title type="html">&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt; &amp;lt;head&amp;gt; ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsgefrql6vwgf0z4q64dym7fnuq02j34cpwul3z9f4fccfdxptvgggzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvvahnga" />
    <content type="html">
      &amp;lt;!DOCTYPE html&amp;gt;&lt;br/&gt;&amp;lt;html lang=&amp;#34;en&amp;#34;&amp;gt;&lt;br/&gt;  &amp;lt;head&amp;gt;&lt;br/&gt;    &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34; /&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:title&amp;#34; content=&amp;#34;n34 - CLI for NIP-34 and Nostr Code Collaboration&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:description&amp;#34; content=&amp;#34;An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:url&amp;#34; content=&amp;#34;&lt;a href=&#34;https://n34.dev&amp;#34;&amp;gt&#34;&gt;https://n34.dev&amp;#34;&amp;gt&lt;/a&gt;;&lt;br/&gt;    &amp;lt;meta property=&amp;#34;og:type&amp;#34; content=&amp;#34;website&amp;#34;&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;twitter:card&amp;#34; content=&amp;#34;summary&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;twitter:title&amp;#34; content=&amp;#34;n34 - CLI for NIP-34 and Nostr Code Collaboration&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;twitter:description&amp;#34; content=&amp;#34;An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.&amp;#34;&amp;gt;&lt;br/&gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34; /&amp;gt;&lt;br/&gt;    &amp;lt;title&amp;gt;n34 - CLI for NIP-34 and Nostr Code Collaboration&amp;lt;/title&amp;gt;&lt;br/&gt;    &amp;lt;style&amp;gt;&lt;br/&gt;      * {&lt;br/&gt;        margin: 0;&lt;br/&gt;        padding: 0;&lt;br/&gt;        box-sizing: border-box;&lt;br/&gt;      }&lt;br/&gt;      body {&lt;br/&gt;        font-family: &amp;#34;Segoe UI&amp;#34;, Tahoma, Geneva, Verdana, sans-serif;&lt;br/&gt;        background: radial-gradient(125% 125% at 50% 90%, #000000 40%, #072607 100%);&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        line-height: 1.6;&lt;br/&gt;        min-height: 100vh;&lt;br/&gt;      }&lt;br/&gt;      .container {&lt;br/&gt;        max-width: 800px;&lt;br/&gt;        margin: 0 auto;&lt;br/&gt;        padding: 60px 20px;&lt;br/&gt;      }&lt;br/&gt;      .hero {&lt;br/&gt;        text-align: center;&lt;br/&gt;        margin-bottom: 80px;&lt;br/&gt;      }&lt;br/&gt;      .hero h1 {&lt;br/&gt;        font-size: 4rem;&lt;br/&gt;        font-weight: 300;&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;        letter-spacing: 2px;&lt;br/&gt;        background: linear-gradient(135deg, #ffffff, #cccccc);&lt;br/&gt;        -webkit-background-clip: text;&lt;br/&gt;        -webkit-text-fill-color: transparent;&lt;br/&gt;        background-clip: text;&lt;br/&gt;      }&lt;br/&gt;      .hero p {&lt;br/&gt;        font-size: 1.2rem;&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        margin-bottom: 40px;&lt;br/&gt;        max-width: 600px;&lt;br/&gt;        margin-left: auto;&lt;br/&gt;        margin-right: auto;&lt;br/&gt;      }&lt;br/&gt;      .buttons {&lt;br/&gt;        display: flex;&lt;br/&gt;        gap: 20px;&lt;br/&gt;        justify-content: center;&lt;br/&gt;        flex-wrap: wrap;&lt;br/&gt;      }&lt;br/&gt;      .btn {&lt;br/&gt;        display: inline-block;&lt;br/&gt;        padding: 14px 28px;&lt;br/&gt;        background: transparent;&lt;br/&gt;        border: 2px solid #ffffff;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        text-decoration: none;&lt;br/&gt;        border-radius: 8px;&lt;br/&gt;        font-weight: 500;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        transition: all 0.3s ease;&lt;br/&gt;        min-width: 120px;&lt;br/&gt;        text-align: center;&lt;br/&gt;      }&lt;br/&gt;      .btn:hover {&lt;br/&gt;        background: #ffffff;&lt;br/&gt;        color: #0a0a0a;&lt;br/&gt;        transform: translateY(-2px);&lt;br/&gt;        box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);&lt;br/&gt;      }&lt;br/&gt;      .section {&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .section h2 {&lt;br/&gt;        font-size: 2.2rem;&lt;br/&gt;        margin-bottom: 30px;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        font-weight: 400;&lt;br/&gt;      }&lt;br/&gt;      .section p {&lt;br/&gt;        font-size: 1.1rem;&lt;br/&gt;        color: #e0e0e0;&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;        text-align: justify;&lt;br/&gt;      }&lt;br/&gt;      a {&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        text-decoration: underline;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        transition: color 0.3s ease;&lt;br/&gt;      }&lt;br/&gt;      a:hover {&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .features {&lt;br/&gt;        display: grid;&lt;br/&gt;        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));&lt;br/&gt;        gap: 30px;&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .feature {&lt;br/&gt;        background: #1a1a1a;&lt;br/&gt;        padding: 30px;&lt;br/&gt;        border-radius: 12px;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        transition: transform 0.3s ease, border-color 0.3s ease;&lt;br/&gt;      }&lt;br/&gt;      .feature:hover {&lt;br/&gt;        transform: translateY(-5px);&lt;br/&gt;        border-color: #555;&lt;br/&gt;      }&lt;br/&gt;      .feature h3 {&lt;br/&gt;        font-size: 1.3rem;&lt;br/&gt;        margin-bottom: 15px;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;        display: flex;&lt;br/&gt;        align-items: center;&lt;br/&gt;        gap: 10px;&lt;br/&gt;      }&lt;br/&gt;      .feature-icon {&lt;br/&gt;        font-size: 1.5rem;&lt;br/&gt;      }&lt;br/&gt;      .feature p {&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        text-align: left;&lt;br/&gt;      }&lt;br/&gt;      .feature-list {&lt;br/&gt;        background: #111;&lt;br/&gt;        padding: 40px;&lt;br/&gt;        border-radius: 12px;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .feature-grid {&lt;br/&gt;        display: grid;&lt;br/&gt;        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));&lt;br/&gt;        gap: 20px;&lt;br/&gt;        margin-top: 20px;&lt;br/&gt;      }&lt;br/&gt;      .feature-column ul {&lt;br/&gt;        list-style: none;&lt;br/&gt;        padding: 0;&lt;br/&gt;      }&lt;br/&gt;      .feature-column li {&lt;br/&gt;        display: flex;&lt;br/&gt;        align-items: center;&lt;br/&gt;        margin-bottom: 12px;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        padding: 8px 0;&lt;br/&gt;      }&lt;br/&gt;      .feature-check {&lt;br/&gt;        display: inline-block;&lt;br/&gt;        width: 20px;&lt;br/&gt;        height: 20px;&lt;br/&gt;        margin-right: 12px;&lt;br/&gt;        border-radius: 3px;&lt;br/&gt;        text-align: center;&lt;br/&gt;        line-height: 20px;&lt;br/&gt;        font-size: 12px;&lt;br/&gt;        font-weight: bold;&lt;br/&gt;        flex-shrink: 0;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.completed {&lt;br/&gt;        background: #22c55e;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.pending {&lt;br/&gt;        background: #374151;&lt;br/&gt;        color: #9ca3af;&lt;br/&gt;        border: 1px solid #4b5563;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.completed::after {&lt;br/&gt;        content: &amp;#34;✓&amp;#34;;&lt;br/&gt;      }&lt;br/&gt;      .feature-check.pending::after {&lt;br/&gt;        content: &amp;#34;○&amp;#34;;&lt;br/&gt;      }&lt;br/&gt;      .feature-text {&lt;br/&gt;        color: #e0e0e0;&lt;br/&gt;      }&lt;br/&gt;      .feature-text.pending {&lt;br/&gt;        color: #9ca3af;&lt;br/&gt;      }&lt;br/&gt;      .install-section {&lt;br/&gt;        background: #111;&lt;br/&gt;        padding: 40px;&lt;br/&gt;        border-radius: 12px;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        margin-bottom: 60px;&lt;br/&gt;      }&lt;br/&gt;      .install-section h2 {&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;        text-align: center;&lt;br/&gt;      }&lt;br/&gt;      .code-block {&lt;br/&gt;        background: #000;&lt;br/&gt;        padding: 20px;&lt;br/&gt;        border-radius: 8px;&lt;br/&gt;        font-family: &amp;#34;Courier New&amp;#34;, monospace;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        border: 1px solid #333;&lt;br/&gt;        overflow-x: auto;&lt;br/&gt;        margin: 15px 0;&lt;br/&gt;      }&lt;br/&gt;      .code-block p {&lt;br/&gt;        color: #00ff00;&lt;br/&gt;      }&lt;br/&gt;      .code-block p::before {&lt;br/&gt;        content: &amp;#34;$ &amp;#34;;&lt;br/&gt;        color: #888;&lt;br/&gt;      }&lt;br/&gt;      .code-block span::before {&lt;br/&gt;        content: &amp;#34;$ &amp;#34;;&lt;br/&gt;        color: #888;&lt;br/&gt;      }&lt;br/&gt;      .code-block span {&lt;br/&gt;        color: #777;&lt;br/&gt;      }&lt;br/&gt;      .code-block pre {&lt;br/&gt;        color: #00ff00;&lt;br/&gt;      }&lt;br/&gt;      .links {&lt;br/&gt;        margin-top: 30px;&lt;br/&gt;      }&lt;br/&gt;      .links h3 {&lt;br/&gt;        font-size: 1.3rem;&lt;br/&gt;        margin-bottom: 15px;&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .links ul {&lt;br/&gt;        list-style: none;&lt;br/&gt;      }&lt;br/&gt;      .links li {&lt;br/&gt;        margin-bottom: 8px;&lt;br/&gt;      }&lt;br/&gt;      .links a {&lt;br/&gt;        color: #cccccc;&lt;br/&gt;        text-decoration: none;&lt;br/&gt;        font-size: 1rem;&lt;br/&gt;        transition: color 0.3s ease;&lt;br/&gt;      }&lt;br/&gt;      .links a:hover {&lt;br/&gt;        color: #ffffff;&lt;br/&gt;      }&lt;br/&gt;      .links a:before {&lt;br/&gt;        content: &amp;#34;→ &amp;#34;;&lt;br/&gt;        margin-right: 8px;&lt;br/&gt;      }&lt;br/&gt;      .status-badge {&lt;br/&gt;        display: inline-block;&lt;br/&gt;        padding: 4px 12px;&lt;br/&gt;        background: #1a4d1a;&lt;br/&gt;        color: #4ade80;&lt;br/&gt;        border-radius: 16px;&lt;br/&gt;        font-size: 0.8rem;&lt;br/&gt;        font-weight: 500;&lt;br/&gt;        margin-bottom: 20px;&lt;br/&gt;      }&lt;br/&gt;      .footer {&lt;br/&gt;        text-align: center;&lt;br/&gt;        padding: 40px 0;&lt;br/&gt;        border-top: 1px solid #333;&lt;br/&gt;        margin-top: 80px;&lt;br/&gt;        color: #888;&lt;br/&gt;      }&lt;br/&gt;      @media (max-width: 768px) {&lt;br/&gt;        .hero h1 {&lt;br/&gt;          font-size: 2.5rem;&lt;br/&gt;        }&lt;br/&gt;        .hero p {&lt;br/&gt;          font-size: 1.1rem;&lt;br/&gt;        }&lt;br/&gt;        .buttons {&lt;br/&gt;          flex-direction: column;&lt;br/&gt;          align-items: center;&lt;br/&gt;        }&lt;br/&gt;        .btn {&lt;br/&gt;          width: 200px;&lt;br/&gt;        }&lt;br/&gt;        .section h2 {&lt;br/&gt;          font-size: 1.8rem;&lt;br/&gt;        }&lt;br/&gt;        .features {&lt;br/&gt;          grid-template-columns: 1fr;&lt;br/&gt;        }&lt;br/&gt;        .feature-grid {&lt;br/&gt;          grid-template-columns: 1fr;&lt;br/&gt;        }&lt;br/&gt;        .install-section,&lt;br/&gt;        .feature-list {&lt;br/&gt;          padding: 20px;&lt;br/&gt;        }&lt;br/&gt;      }&lt;br/&gt;    &amp;lt;/style&amp;gt;&lt;br/&gt;  &amp;lt;/head&amp;gt;&lt;br/&gt;  &amp;lt;body&amp;gt;&lt;br/&gt;    &amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;hero&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;status-badge&amp;#34;&amp;gt;✨ Looking for feedback&amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;h1&amp;gt;n34&amp;lt;/h1&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;&lt;br/&gt;          n34 is an open source command-line interface (CLI) tool for&lt;br/&gt;          sending and receiving Git issues, patches and comments over the&lt;br/&gt;          Nostr protocol. It supports creating, replying to, and managing&lt;br/&gt;          issues and patches, making Git collaboration decentralized and&lt;br/&gt;          censorship-resistant.&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;buttons&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://n34.dev/commands.html&amp;#34&#34;&gt;https://n34.dev/commands.html&amp;#34&lt;/a&gt;; class=&amp;#34;btn&amp;#34;&amp;gt;Documentation&amp;lt;/a&amp;gt;&lt;br/&gt;          &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;#34&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;#34&lt;/a&gt;; class=&amp;#34;btn&amp;#34;&amp;gt;Git Repository&amp;lt;/a&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;section&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Key Features&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;features&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🔄&amp;lt;/span&amp;gt;Complete Git Workflow&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;Handle the full development lifecycle with patches, issues, replies, and status tracking, all through the decentralized Nostr protocol.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🔗&amp;lt;/span&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-34&amp;lt;/a&amp;gt; Compliant&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;Fully implements the &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/34.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-34&amp;lt;/a&amp;gt; specification for Git repositories on Nostr, ensuring compatibility with the decentralized ecosystem.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🛠️&amp;lt;/span&amp;gt;Developer Friendly&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;Intuitive CLI interface designed for developers who want to integrate Git workflows with Nostr seamlessly.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h3&amp;gt;&amp;lt;span class=&amp;#34;feature-icon&amp;#34;&amp;gt;🔐&amp;lt;/span&amp;gt;Self-Sovereign&amp;lt;/h3&amp;gt;&lt;br/&gt;            &amp;lt;p&amp;gt;No accounts, no passwords, no centralized servers. You control your identity and data through cryptographic keys.&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;feature-list&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Feature Roadmap&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 30px; color: #cccccc;&amp;#34;&amp;gt;Current implementation status and upcoming features&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;feature-grid&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature-column&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;ul&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Repository announcements&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check pending&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Repository state announcements&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Patches (Send, fetch and list)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Issues (Send, view and list)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Replies&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Issues and patches status&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check pending&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Pull requests (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/pull/1966&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/pull/1966&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;nostr-protocol/nips#1966&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;            &amp;lt;/ul&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;          &amp;lt;div class=&amp;#34;feature-column&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;ul&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Gossip Model (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/65.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/65.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-65&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Proof of Work (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/13.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/13.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-13&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;nostr: URI scheme (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/21.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/21.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-21&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Signing using bunker (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/46.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/46.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-46&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Signing using &amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/07.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/07.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-07&amp;lt;/a&amp;gt; proxy (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://crates.io/crates/nostr-browser-signer-proxy&amp;#34&#34;&gt;https://crates.io/crates/nostr-browser-signer-proxy&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;nostr-browser-signer-proxy&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;Secret key keyring&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check pending&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text pending&amp;#34;&amp;gt;Code Snippets (&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://github.com/nostr-protocol/nips/blob/master/C0.md&amp;#34&#34;&gt;https://github.com/nostr-protocol/nips/blob/master/C0.md&amp;#34&lt;/a&gt;; target=&amp;#34;_blank&amp;#34; rel=&amp;#34;noreferrer&amp;#34;&amp;gt;NIP-C0&amp;lt;/a&amp;gt;)&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;              &amp;lt;li&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-check completed&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;&lt;br/&gt;                &amp;lt;span class=&amp;#34;feature-text&amp;#34;&amp;gt;In device relays and repos bookmark&amp;lt;/span&amp;gt;&lt;br/&gt;              &amp;lt;/li&amp;gt;&lt;br/&gt;            &amp;lt;/ul&amp;gt;&lt;br/&gt;          &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;install-section&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Quick Start&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 20px; color: #cccccc;&amp;#34;&amp;gt;Get started with n34 in seconds&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;p&amp;gt;cargo install n34&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;span&amp;gt;# The execautable will be in ~/.cargo/bin/n34&amp;lt;/span&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin: 20px 0; color: #888;&amp;#34;&amp;gt;or&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;p&amp;gt;git clone &lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&lt;/a&gt;;&lt;br/&gt;          &amp;lt;p&amp;gt;cd n34 &amp;amp;&amp;amp; cargo build --release&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;span&amp;gt;# The execautable will be in target/release/n34&amp;lt;/span&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin: 20px 0; color: #888;&amp;#34;&amp;gt;NixOS (Version 0.4.0 or later)&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;p&amp;gt;git clone &lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&#34;&gt;https://git.4rs.nl/awiteb/n34.git&amp;lt;/p&amp;gt&lt;/a&gt;;&lt;br/&gt;          &amp;lt;p&amp;gt;cd n34 &amp;amp;&amp;amp; nix build&amp;lt;/p&amp;gt;&lt;br/&gt;          &amp;lt;span&amp;gt;# The execautable will be in result/bin/n34&amp;lt;/span&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin: 20px 0; color: #888;&amp;#34;&amp;gt;home-manager&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 20px; color: #cccccc;&amp;#34;&amp;gt;Add this to your &amp;lt;code&amp;gt;flake.nix&amp;lt;/code&amp;gt; inputs&lt;br/&gt;        &amp;lt;br&amp;gt;&lt;br/&gt;Specify the version you want to install, or remove &amp;lt;code&amp;gt;?ref&amp;lt;/code&amp;gt; for the unreleased version. You can also use any mirror; it doesn&amp;#39;t have to be &amp;lt;code&amp;gt;git.4rs.nl&amp;lt;/code&amp;gt;&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;pre&amp;gt;&lt;br/&gt;inputs = {&lt;br/&gt;  n34.url = &amp;#34;git&#43;&lt;a href=&#34;https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x&amp;#34&#34;&gt;https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x&amp;#34&lt;/a&gt;;;&lt;br/&gt;};&amp;lt;/pre&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-bottom: 20px; color: #cccccc;&amp;#34;&amp;gt;And this in your home packages&amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;code-block&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;pre&amp;gt;packages = [ inputs.n34.packages.&amp;#34;${pkgs.system}&amp;#34;.default ];&amp;lt;/pre&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;p style=&amp;#34;text-align: center; margin-top: 20px; color: #cccccc;&amp;#34;&amp;gt;Once installed, run &amp;lt;code style=&amp;#34;background: #333; padding: 2px 6px; border-radius: 4px;&amp;#34;&amp;gt;n34 --help&amp;lt;/code&amp;gt; to see available commands.&amp;lt;/p&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;section&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;h2&amp;gt;Why Nostr?&amp;lt;/h2&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;&lt;br/&gt;          Nostr is fundamentally different from traditional platforms because it&amp;#39;s not an application or service, it&amp;#39;s a decentralized protocol. This means any tool or app can integrate with it, enabling open, permissionless collaboration&lt;br/&gt;          without relying on centralized gatekeepers. Unlike proprietary systems, Nostr doesn&amp;#39;t require emails, passwords, or accounts. You interact directly through relays, whether you self-host your own or use public ones, ensuring no&lt;br/&gt;          single point of failure or control.&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;&lt;br/&gt;          What makes Nostr uniquely resilient is its design, the protocol itself is just a set of rules, not a company or product that can disappear. Your Git issues, patches, and comments persist as long as relays choose to store them,&lt;br/&gt;          immune to the whims of corporate shutdowns or policy changes. Nostr is infrastructure in its purest form, an idea that outlives any temporary implementation. n34 taps into a future-proof foundation for decentralized collaboration.&lt;br/&gt;        &amp;lt;/p&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;links&amp;#34;&amp;gt;&lt;br/&gt;          &amp;lt;h3&amp;gt;More about Nostr&amp;lt;/h3&amp;gt;&lt;br/&gt;          &amp;lt;ul&amp;gt;&lt;br/&gt;            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://nostr.com&amp;#34;&amp;gt;nostr.com&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&#34;&gt;https://nostr.com&amp;#34;&amp;gt;nostr.com&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&lt;/a&gt;;&lt;br/&gt;            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://nostr.org&amp;#34;&amp;gt;nostr.org&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&#34;&gt;https://nostr.org&amp;#34;&amp;gt;nostr.org&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&lt;/a&gt;;&lt;br/&gt;            &amp;lt;li&amp;gt;&amp;lt;a href=&amp;#34;&lt;a href=&#34;https://nostr.how/en/what-is-nostr&amp;#34;&amp;gt;nostr.how/en/what-is-nostr&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&#34;&gt;https://nostr.how/en/what-is-nostr&amp;#34;&amp;gt;nostr.how/en/what-is-nostr&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt&lt;/a&gt;;&lt;br/&gt;          &amp;lt;/ul&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;      &amp;lt;div class=&amp;#34;footer&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;p&amp;gt;Built with ❤️ for the decentralized future&amp;lt;/p&amp;gt;&lt;br/&gt;      &amp;lt;/div&amp;gt;&lt;br/&gt;    &amp;lt;/div&amp;gt;&lt;br/&gt;  &amp;lt;/body&amp;gt;&lt;br/&gt;&amp;lt;/html&amp;gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:22Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs88j6qtspp228grzquf0fk097rjvcf329agulf8v2lfsnvtwyml4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvwamx4z</id>
    
      <title type="html"># Merge an Open Patch &amp;gt; `n34 patch merge` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs88j6qtspp228grzquf0fk097rjvcf329agulf8v2lfsnvtwyml4czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvwamx4z" />
    <content type="html">
      # Merge an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch merge` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Set an open patch status to merged&lt;br/&gt;&lt;br/&gt;Usage: n34 patch merge [OPTIONS] &amp;lt;PATCH_ID&amp;gt; &amp;lt;MERGE_COMMIT&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;      The open patch id to merge it. Must be orignal root patch or revision root&lt;br/&gt;  &amp;lt;MERGE_COMMIT&amp;gt;  The merge commit id&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --patches &amp;lt;PATCH-EVENT-ID&amp;gt;   Patches that have been merged. Use this when only some patches have been merged, not all&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Creates a kind `1631` event (Applied/Merged status) for the specified patch. The&lt;br/&gt;patch must be in open status.&lt;br/&gt;&lt;br/&gt;You can specify either an original patch or revision patch ID, but the status&lt;br/&gt;event will only reference the original patch. Revision patches will be mentioned&lt;br/&gt;in the event.&lt;br/&gt;&lt;br/&gt;You can get the `MERGE_COMMIT` commit using `git rev-parse HEAD` command if&lt;br/&gt;the merge commit in the `HEAD` or use `HEAD~n` where the `n` is the number of&lt;br/&gt;commits the merge commit before the HEAD&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:22Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsw622puayqgg00h9zsf7f6hqucd2khpmq2ysg8wyan2kwwxy2av2qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgefcrm</id>
    
      <title type="html"># Apply an Open Patch &amp;gt; `n34 patch apply` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsw622puayqgg00h9zsf7f6hqucd2khpmq2ysg8wyan2kwwxy2av2qzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvgefcrm" />
    <content type="html">
      # Apply an Open Patch&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch apply` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Set an open patch status to applied&lt;br/&gt;&lt;br/&gt;Usage: n34 patch apply [OPTIONS] &amp;lt;PATCH_ID&amp;gt; [APPLIED_COMMITS]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;            The open patch id to apply it. Must be orignal root patch or revision root&lt;br/&gt;  [APPLIED_COMMITS]...  The applied commits&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;      --patches &amp;lt;PATCH-EVENT-ID&amp;gt;   Patches that have been applied. Use this when only some patches have been applied, not all&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Creates a kind `1631` event (Applied/Merged status) for the specified patch. The&lt;br/&gt;patch must be in open status.&lt;br/&gt;&lt;br/&gt;You can specify either an original patch or revision patch ID, but the status&lt;br/&gt;event will only reference the original patch. Revision patches will be mentioned&lt;br/&gt;in the event.&lt;br/&gt;&lt;br/&gt;The `APPLIED_COMMITS` field serves to inform clients about the status of&lt;br/&gt;specific commits, whether they have been applied or not. If you need to retrieve&lt;br/&gt;the list of commits from a specific point (such as the tip of the master branch)&lt;br/&gt;up to the `HEAD`, you can use the following Git command: `git log --pretty=%H&lt;br/&gt;&amp;#39;origin/master..HEAD&amp;#39;`.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:19Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs26cu0w2u06n2k8q5w290acl342rlzx8h8p5kyeyu0ymhxw9maj8gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdf2ygn</id>
    
      <title type="html">use rand_chacha::ChaCha20Rng; use ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs26cu0w2u06n2k8q5w290acl342rlzx8h8p5kyeyu0ymhxw9maj8gzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvdf2ygn" />
    <content type="html">
      use rand_chacha::ChaCha20Rng;&lt;br/&gt;use rand_chacha::rand_core::SeedableRng;&lt;br/&gt;use frost_secp256k1_tr as frost;&lt;br/&gt;use frost::{Identifier, keys::IdentifierList, round1, round2};&lt;br/&gt;use std::collections::BTreeMap;&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    let mut dealer_rng = ChaCha20Rng::from_seed([0u8; 32]);&lt;br/&gt;    let min_signers = 2;&lt;br/&gt;    let (shares, pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        3, min_signers, IdentifierList::Default, &amp;amp;mut dealer_rng&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    // 1. Setup Signer (P1) and peer (P2)&lt;br/&gt;    let p1_id = Identifier::try_from(1u16)?;&lt;br/&gt;    let p2_id = Identifier::try_from(2u16)?;&lt;br/&gt;    &lt;br/&gt;    // 2. Round 1: Both signers generate nonces (Simulating P2&amp;#39;s contribution)&lt;br/&gt;    let mut rng1 = ChaCha20Rng::from_seed([1u8; 32]);&lt;br/&gt;    let (p1_nonces, p1_commitments) = round1::commit(shares[&amp;amp;p1_id].signing_share(), &amp;amp;mut rng1);&lt;br/&gt;&lt;br/&gt;    let mut rng2 = ChaCha20Rng::from_seed([2u8; 32]);&lt;br/&gt;    let (_p2_nonces, p2_commitments) = round1::commit(shares[&amp;amp;p2_id].signing_share(), &amp;amp;mut rng2);&lt;br/&gt;&lt;br/&gt;    // 3. Coordinator: Create a valid SigningPackage with 2 signers&lt;br/&gt;    let message = b&amp;#34;gnostr-gcc-verification-test&amp;#34;;&lt;br/&gt;    let mut commitments_map = BTreeMap::new();&lt;br/&gt;    commitments_map.insert(p1_id, p1_commitments);&lt;br/&gt;    commitments_map.insert(p2_id, p2_commitments); // Added P2 to satisfy threshold&lt;br/&gt;    &lt;br/&gt;    let signing_package = frost::SigningPackage::new(commitments_map, message);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;--- BIP-64MOD Round 2: Signer Validation ---&amp;#34;);&lt;br/&gt;&lt;br/&gt;    // 4. SIGNER-SIDE CHECK (Manual)&lt;br/&gt;    if !signing_package.signing_commitments().contains_key(&amp;amp;p1_id) {&lt;br/&gt;        return Err(&amp;#34;Validation Failed: My commitment is missing!&amp;#34;.into());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    let commitment_count = signing_package.signing_commitments().len() as u16;&lt;br/&gt;    if commitment_count &amp;lt; min_signers {&lt;br/&gt;         return Err(format!(&amp;#34;Validation Failed: Only {} commitments provided, need {}.&amp;#34;, commitment_count, min_signers).into());&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;✅ Signing Package validated ({} signers).&amp;#34;, commitment_count);&lt;br/&gt;    println!(&amp;#34;Proceeding to sign message: {:?}&amp;#34;, String::from_utf8_lossy(message));&lt;br/&gt;&lt;br/&gt;    // 5. Generate the Share&lt;br/&gt;    let p1_verifying_share = frost::keys::VerifyingShare::from(*shares[&amp;amp;p1_id].signing_share());&lt;br/&gt;    let p1_key_package = frost::keys::KeyPackage::new(&lt;br/&gt;        p1_id,&lt;br/&gt;        *shares[&amp;amp;p1_id].signing_share(),&lt;br/&gt;        p1_verifying_share,&lt;br/&gt;        *pubkey_package.verifying_key(),&lt;br/&gt;        min_signers,&lt;br/&gt;    );&lt;br/&gt;&lt;br/&gt;    let p1_signature_share = round2::sign(&amp;amp;signing_package, &amp;amp;p1_nonces, &amp;amp;p1_key_package)?;&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;\nPartial Signature Share for P1:&amp;#34;);&lt;br/&gt;    println!(&amp;#34;{}&amp;#34;, hex::encode(p1_signature_share.serialize()));&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(not(feature = &amp;#34;nostr&amp;#34;))]&lt;br/&gt;fn main() { println!(&amp;#34;Enable nostr feature.&amp;#34;); }&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:12Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs9qgugs7espepymuczry6kgppmh0sa4u5fcayzd45afve5x6hg7eqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvltx5m8</id>
    
      <title type="html"># Fallback Relays &amp;gt; `n34 config relays` command **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs9qgugs7espepymuczry6kgppmh0sa4u5fcayzd45afve5x6hg7eqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvltx5m8" />
    <content type="html">
      # Fallback Relays&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config relays` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Sets the default fallback relays if none provided. Use this relays for read and write&lt;br/&gt;&lt;br/&gt;Usage: n34 config relays [OPTIONS] [RELAYS]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [RELAYS]...  List of relay URLs to append to fallback relays. If empty, removes all fallback relays&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --override  Replace existing fallback relays instead of appending new ones&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command configures the default fallback relays, which `n34` uses to read&lt;br/&gt;from and write to. To add relays, provide their URLs as arguments to append&lt;br/&gt;them to the current list. Use the `--override` flag to replace the existing list&lt;br/&gt;entirely. To clear all fallback relays, run the command without any arguments.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:12Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsthgfmrug9kj9gkrs7yda5ttf9xg3t5jh7ydj5wp324ks2prqw0uszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvangefd</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsthgfmrug9kj9gkrs7yda5ttf9xg3t5jh7ydj5wp324ks2prqw0uszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvangefd" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;/// `config bunker` subcommand&lt;br/&gt;mod bunker;&lt;br/&gt;/// `config keyring` subcommand&lt;br/&gt;mod keyring;&lt;br/&gt;/// `config nip07` subcommand&lt;br/&gt;mod nip07;&lt;br/&gt;/// `config pow` subcommand&lt;br/&gt;mod pow;&lt;br/&gt;/// `config relays` subcommand&lt;br/&gt;mod relays;&lt;br/&gt;&lt;br/&gt;use clap::Subcommand;&lt;br/&gt;&lt;br/&gt;use self::bunker::BunkerArgs;&lt;br/&gt;use self::keyring::KeyringArgs;&lt;br/&gt;use self::nip07::Nip07Args;&lt;br/&gt;use self::pow::PowArgs;&lt;br/&gt;use self::relays::RelaysArgs;&lt;br/&gt;use super::CliOptions;&lt;br/&gt;use crate::{cli::traits::CommandRunner, error::N34Result};&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;#[derive(Subcommand, Debug)]&lt;br/&gt;pub enum ConfigSubcommands {&lt;br/&gt;    /// Sets the default PoW difficulty (0 if not specified)&lt;br/&gt;    Pow(PowArgs),&lt;br/&gt;    /// Sets the default fallback relays if none provided. Use this relays for&lt;br/&gt;    /// read and write.&lt;br/&gt;    Relays(RelaysArgs),&lt;br/&gt;    /// Sets a URL of NIP-46 bunker server used for signing events.&lt;br/&gt;    Bunker(BunkerArgs),&lt;br/&gt;    /// Managing the secret key keyring, including enabling, disabling, or&lt;br/&gt;    /// resetting it.&lt;br/&gt;    Keyring(KeyringArgs),&lt;br/&gt;    /// Controls the NIP-07 browser signer proxy, turning it on or off, and&lt;br/&gt;    /// configures the `ip:port` address.&lt;br/&gt;    Nip07(Nip07Args),&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for ConfigSubcommands {&lt;br/&gt;    async fn run(self, options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        crate::run_command!(self, options, &amp;amp; Pow Relays Bunker Keyring Nip07)&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:10Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqstfks8mnz2fh0nsy6h9ch4pplqxhzvw59s9dv7y2e490j4jz8vejgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4rscjn</id>
    
      <title type="html"># List Repositories Issues &amp;gt; `n34 issue list` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqstfks8mnz2fh0nsy6h9ch4pplqxhzvw59s9dv7y2e490j4jz8vejgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv4rscjn" />
    <content type="html">
      # List Repositories Issues&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue list` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;List the repositories issues&lt;br/&gt;&lt;br/&gt;Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NADDR-NIP05-OR-SET]...  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --limit &amp;lt;LIMIT&amp;gt;  Maximum number of issues to list [default: 15]&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;List the repositories issues. By default `n34` will look for `nostr-address`&lt;br/&gt;file and extract the repositories from it.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:10Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqstyhzckey9s9f5jtlf7uzj5wfkwnkwh9833ye9tqkdmc2334u6ekszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvj2wsxr</id>
    
      <title type="html"># List Repositories Patches &amp;gt; `n34 patch list` command ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqstyhzckey9s9f5jtlf7uzj5wfkwnkwh9833ye9tqkdmc2334u6ekszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvj2wsxr" />
    <content type="html">
      # List Repositories Patches&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch list` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;List the repositories patches&lt;br/&gt;&lt;br/&gt;Usage: n34 patch list [OPTIONS] [NADDR-NIP05-OR-SET]...&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  [NADDR-NIP05-OR-SET]...  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --limit &amp;lt;LIMIT&amp;gt;  Maximum number of patches to list [default: 15]&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;List the repositories patches. By default `n34` will look for `nostr-address`&lt;br/&gt;file and extract the repositories from it.&lt;br/&gt;&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:10Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsz2qp6yecc96yqzp4yws93s422jyk5s0uuv7vwugt3jhj05ke6jcczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3rgjck</id>
    
      <title type="html"># Patch Management In `n34`, patch management is designed to give ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsz2qp6yecc96yqzp4yws93s422jyk5s0uuv7vwugt3jhj05ke6jcczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsv3rgjck" />
    <content type="html">
      # Patch Management&lt;br/&gt;&lt;br/&gt;In `n34`, patch management is designed to give you complete control. You can&lt;br/&gt;manually generate patch files using `git-format-patch` and then broadcast them&lt;br/&gt;to Nostr relays. This ensures that you have full authority over the content&lt;br/&gt;and structure of your patches, allowing for precise customization as per your&lt;br/&gt;requirements.&lt;br/&gt;&lt;br/&gt;Similarly, when fetching patches, `n34` provides them to you without&lt;br/&gt;automatically applying, merging, or checking them. This empowers you to review&lt;br/&gt;the patches at your own pace and decide whether to merge or apply them as&lt;br/&gt;needed. You retain full control over the entire process, ensuring a tailored&lt;br/&gt;approach to patch management.&lt;br/&gt;&lt;br/&gt;## Patch Status Management&lt;br/&gt;&lt;br/&gt;You can assign a status to original patches, but revision patches do not have&lt;br/&gt;a specific status assigned to them. Instead, they inherit the status of the&lt;br/&gt;original patch. However, if the original patch is marked as `Applied/Merged`,&lt;br/&gt;the revision patch must be explicitly tagged to claim the same status. If not&lt;br/&gt;tagged, the revision patch status will be `Closed`.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:08Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsyx4k9l7hgxzww3q7zgpldwwagk8knzhvewr9ccm6hzftcgu3kzhczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvp647zw</id>
    
      <title type="html">use rand_chacha::ChaCha20Rng; use ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsyx4k9l7hgxzww3q7zgpldwwagk8knzhvewr9ccm6hzftcgu3kzhczyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvp647zw" />
    <content type="html">
      use rand_chacha::ChaCha20Rng;&lt;br/&gt;use rand_chacha::rand_core::SeedableRng;&lt;br/&gt;use frost_secp256k1_tr as frost;&lt;br/&gt;use frost::{Identifier, keys::IdentifierList, round1};&lt;br/&gt;use std::collections::BTreeMap;&lt;br/&gt;&lt;br/&gt;#[cfg(feature = &amp;#34;nostr&amp;#34;)]&lt;br/&gt;fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {&lt;br/&gt;    // 1. Setup deterministic dealer (Genesis State)&lt;br/&gt;    let mut dealer_rng = ChaCha20Rng::from_seed([0u8; 32]);&lt;br/&gt;    let (shares, _pubkey_package) = frost::keys::generate_with_dealer(&lt;br/&gt;        3, 2, IdentifierList::Default, &amp;amp;mut dealer_rng&lt;br/&gt;    )?;&lt;br/&gt;&lt;br/&gt;    // 2. Setup Participant 1&lt;br/&gt;    let p1_id = Identifier::try_from(1u16)?;&lt;br/&gt;    let p1_share = &amp;amp;shares[&amp;amp;p1_id];&lt;br/&gt;    &lt;br/&gt;    // 3. Setup Nonce RNG&lt;br/&gt;    let mut nonce_rng = ChaCha20Rng::from_seed([1u8; 32]);&lt;br/&gt;&lt;br/&gt;    println!(&amp;#34;--- BIP-64MOD Round 1: Batch Nonce Generation ---&amp;#34;);&lt;br/&gt;    println!(&amp;#34;Participant: {:?}&amp;#34;, p1_id);&lt;br/&gt;    println!(&amp;#34;Generating 10 Nonce Pairs...\n&amp;#34;);&lt;br/&gt;&lt;br/&gt;    let mut batch_commitments = BTreeMap::new();&lt;br/&gt;    let mut batch_secrets = Vec::new();&lt;br/&gt;&lt;br/&gt;    for i in 0..10 {&lt;br/&gt;        // Generate a single pair&lt;br/&gt;        let (nonces, commitments) = round1::commit(p1_share.signing_share(), &amp;amp;mut nonce_rng);&lt;br/&gt;        &lt;br/&gt;        // Store the secret nonces locally (index i)&lt;br/&gt;        batch_secrets.push(nonces);&lt;br/&gt;        &lt;br/&gt;        // Store the public commitments in a map to share with the Coordinator&lt;br/&gt;        batch_commitments.insert(i, commitments);&lt;br/&gt;&lt;br/&gt;        println!(&amp;#34;Nonce Pair [{}]:&amp;#34;, i);&lt;br/&gt;        println!(&amp;#34;  Hiding:  {}&amp;#34;, hex::encode(commitments.hiding().serialize()?));&lt;br/&gt;        println!(&amp;#34;  Binding: {}&amp;#34;, hex::encode(commitments.binding().serialize()?));&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    // 4. Persistence Simulation&lt;br/&gt;    // In a real GCC app, you would save `batch_secrets` to an encrypted file &lt;br/&gt;    // and send `batch_commitments` to a Nostr Relay (Kind 1351).&lt;br/&gt;    println!(&amp;#34;\n✅ Batch generation complete.&amp;#34;);&lt;br/&gt;    println!(&amp;#34;Ready to sign up to 10 independent Git commits.&amp;#34;);&lt;br/&gt;&lt;br/&gt;    Ok(())&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;#[cfg(not(feature = &amp;#34;nostr&amp;#34;))]&lt;br/&gt;fn main() { println!(&amp;#34;Run with --features nostr&amp;#34;); }&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:01Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsraw5zv7g6x2gj4m4ehk62drcjyeplf9c5f6v05z4ekdxga5s963czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfxuplx</id>
    
      <title type="html"># Default PoW Difficulty &amp;gt; `n34 config pow` **Usage:** ``` ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsraw5zv7g6x2gj4m4ehk62drcjyeplf9c5f6v05z4ekdxga5s963czyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfxuplx" />
    <content type="html">
      # Default PoW Difficulty&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 config pow`&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Sets the default PoW difficulty (0 if not specified)&lt;br/&gt;&lt;br/&gt;Usage: n34 config pow &amp;lt;DIFFICULTY&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;DIFFICULTY&amp;gt;  The new default PoW difficulty&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;This command configures the default Proof of Work (PoW) difficulty for newly&lt;br/&gt;created events. This setting is applied to most generated events, but it&lt;br/&gt;intentionally skips patch events. Because patches can be numerous, calculating&lt;br/&gt;PoW for each one would significantly slow down operations.&lt;br/&gt;&lt;br/&gt;If you want to disable the PoW just make it 0.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:34:00Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs2psz6qgfyswkvgzpcx44w4zsd2xt79xeqds022rq0qm6m00ux8yszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfqcfdj</id>
    
      <title type="html">// n34 - A CLI to interact with NIP-34 and other stuff related to ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs2psz6qgfyswkvgzpcx44w4zsd2xt79xeqds022rq0qm6m00ux8yszyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvfqcfdj" />
    <content type="html">
      // n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr&lt;br/&gt;// Copyright (C) 2025 Awiteb &amp;lt;a@4rs.nl&amp;gt;&lt;br/&gt;//&lt;br/&gt;// This program is free software: you can redistribute it and/or modify&lt;br/&gt;// it under the terms of the GNU General Public License as published by&lt;br/&gt;// the Free Software Foundation, either version 3 of the License, or&lt;br/&gt;// (at your option) any later version.&lt;br/&gt;//&lt;br/&gt;// This program is distributed in the hope that it will be useful,&lt;br/&gt;// but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;br/&gt;// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;br/&gt;// GNU General Public License for more details.&lt;br/&gt;//&lt;br/&gt;// You should have received a copy of the GNU General Public License&lt;br/&gt;// along with this program. If not, see &amp;lt;&lt;a href=&#34;https://gnu.org/licenses/gpl-3.0.html&amp;gt&#34;&gt;https://gnu.org/licenses/gpl-3.0.html&amp;gt&lt;/a&gt;;.&lt;br/&gt;&lt;br/&gt;use clap::{ArgGroup, Args};&lt;br/&gt;&lt;br/&gt;use crate::{&lt;br/&gt;    cli::{Cli, CliOptions, traits::CommandRunner},&lt;br/&gt;    error::N34Result,&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;#[derive(Args, Debug)]&lt;br/&gt;#[clap(&lt;br/&gt;    group(&lt;br/&gt;        ArgGroup::new(&amp;#34;options&amp;#34;)&lt;br/&gt;            .required(true)&lt;br/&gt;    )&lt;br/&gt;)]&lt;br/&gt;pub struct KeyringArgs {&lt;br/&gt;    /// Turns on secret key keyring. Requires entering the key once when&lt;br/&gt;    /// enabled.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    enable:  bool,&lt;br/&gt;    /// Turns off secret key keyring. Removes any existing key and prevents&lt;br/&gt;    /// storing new ones.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    disable: bool,&lt;br/&gt;    /// Deletes current key and stores the next provided key.&lt;br/&gt;    #[arg(long, group = &amp;#34;options&amp;#34;)]&lt;br/&gt;    reset:   bool,&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;impl CommandRunner for KeyringArgs {&lt;br/&gt;    const NEED_SIGNER: bool = false;&lt;br/&gt;&lt;br/&gt;    async fn run(self, mut options: CliOptions) -&amp;gt; N34Result&amp;lt;()&amp;gt; {&lt;br/&gt;        let keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME);&lt;br/&gt;&lt;br/&gt;        if self.enable {&lt;br/&gt;            options.config.keyring_secret_key = true;&lt;br/&gt;        } else if self.disable {&lt;br/&gt;            options.config.keyring_secret_key = false;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        if self.reset || self.disable {&lt;br/&gt;            let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        options.config.dump()&lt;br/&gt;    }&lt;br/&gt;}&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:33:59Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsfk73v3h3n3nuhracpk7j0rdzs4clkut72q5lxck7kwd4wlr9jvaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvu9nv2e</id>
    
      <title type="html"># Closes an Open Issue &amp;gt; `n34 issue close` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsfk73v3h3n3nuhracpk7j0rdzs4clkut72q5lxck7kwd4wlr9jvaqzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvu9nv2e" />
    <content type="html">
      # Closes an Open Issue&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 issue close` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Closes an open issue&lt;br/&gt;&lt;br/&gt;Usage: n34 issue close [OPTIONS] &amp;lt;ISSUE_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;ISSUE_ID&amp;gt;  The open issue id to close it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Issue a kind `1632` (Close status) for the specified issue. The issue have to&lt;br/&gt;be open.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:33:59Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsq2whetfv8ufxvn5aacuq5h3s6l6y52ms0lgc30vutsh4aj44h4rgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvlumzf6</id>
    
      <title type="html"># Fetch a Patch By ID &amp;gt; `n34 patch fetch` command **Usage:** ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsq2whetfv8ufxvn5aacuq5h3s6l6y52ms0lgc30vutsh4aj44h4rgzyptmj32nj8lt658t74lretj59vennwa847lj5yjgfwvdmesyy8dsvlumzf6" />
    <content type="html">
      # Fetch a Patch By ID&lt;br/&gt;&lt;br/&gt;&amp;gt; `n34 patch fetch` command&lt;br/&gt;&lt;br/&gt;**Usage:**&lt;br/&gt;```&lt;br/&gt;Fetches a patch by its id&lt;br/&gt;&lt;br/&gt;Usage: n34 patch fetch [OPTIONS] &amp;lt;PATCH_ID&amp;gt;&lt;br/&gt;&lt;br/&gt;Arguments:&lt;br/&gt;  &amp;lt;PATCH_ID&amp;gt;  The patch id to fetch it&lt;br/&gt;&lt;br/&gt;Options:&lt;br/&gt;      --repo &amp;lt;NADDR-NIP05-OR-SET&amp;gt;  Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`&lt;br/&gt;  -o, --output &amp;lt;PATH&amp;gt;              Output directory for the patches. Default to the current directory&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;Fetches patches using their original patch ID. All fetched patches will be saved&lt;br/&gt;to the specified output directory (current directory by default). You can then&lt;br/&gt;apply or merge these patches into your branch as needed.&lt;br/&gt;
    </content>
    <updated>2026-04-05T04:33:59Z</updated>
  </entry>

</feed>