<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stanislav(Stas) Katkov</title>
    <description>The latest articles on DEV Community by Stanislav(Stas) Katkov (@skatkov).</description>
    <link>https://dev.to/skatkov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44564%2Fb9a24636-ce03-4dc4-aadf-5935e4c60d39.png</url>
      <title>DEV Community: Stanislav(Stas) Katkov</title>
      <link>https://dev.to/skatkov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skatkov"/>
    <language>en</language>
    <item>
      <title>Software License management with Polar.sh</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Sun, 11 May 2025 17:48:12 +0000</pubDate>
      <link>https://dev.to/skatkov/software-license-management-with-polarsh-eb8</link>
      <guid>https://dev.to/skatkov/software-license-management-with-polarsh-eb8</guid>
      <description>&lt;p&gt;This article would have been incredibly helpful to me just a month ago—so here it is, primarily for other developers who, like me, don't usually write software that's meant to run in non-server environments. &lt;/p&gt;

&lt;p&gt;I just introduced licenses to one of my apps—&lt;a href="https://devtui.com" rel="noopener noreferrer"&gt;DevTUI&lt;/a&gt;. There's another app that suffers from my code, &lt;a href="https://poshtui.com" rel="noopener noreferrer"&gt;PoshTUI&lt;/a&gt;, and it might require something similar soon. I suspect all of this will come in handy again in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Starting out, I only knew what needed to be built: a licensing solution for a one-time payment product. A solution should support the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ability to issue free licenses to early supporters of &lt;a href="https://poshtui.com" rel="noopener noreferrer"&gt;PoshTUI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ability to limit each license to three machines (or less)&lt;/li&gt;
&lt;li&gt;Allow customers to manage their licenses without a need to contact support(e.g. deactivate licenses or view receipts)&lt;/li&gt;
&lt;li&gt;Should work offline; license validation should not run every time the app starts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Licensing Server
&lt;/h2&gt;

&lt;p&gt;I'm using &lt;a href="https://polar.sh" rel="noopener noreferrer"&gt;Polar.sh&lt;/a&gt; as my payment and licensing provider. I'm skipping the part about how I ended up with them—no interest in badmouthing any competitors.&lt;/p&gt;

&lt;p&gt;Rolling your own solution on top of Stripe is also possible. But that wasn't an option worth pursuing for me. A 4% cut of future revenue seemed like a fair trade-off to avoid dealing with payment handling and license infrastructure.&lt;/p&gt;

&lt;p&gt;Polar team has been incredibly helpful. When I first contacted them, they didn’t even have a Go SDK, but they published one within two days. They also responded with useful tips and shipped fixes—again, all within 48 hours of my initial email.&lt;/p&gt;

&lt;p&gt;Polar.sh provides me with all the server side niceties.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offer a way to define a product that can be paid one time&lt;/li&gt;
&lt;li&gt;There is also  Discounts for those early supporters and friends&lt;/li&gt;
&lt;li&gt;&lt;a href="https://polar.sh/krooni/portal/request" rel="noopener noreferrer"&gt;Customer portal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://buy.polar.sh/polar_cl_JPBTnQKWsNBC8lA7tpR1uZYne5hMuW40xqTRI3P9WcH" rel="noopener noreferrer"&gt;Checkout links&lt;/a&gt; for a product, even one with a 100% discount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the client-side implementation is up to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  License Activation
&lt;/h2&gt;

&lt;p&gt;The app can do three license related actions - &lt;strong&gt;active&lt;/strong&gt;, &lt;strong&gt;validate&lt;/strong&gt; and &lt;strong&gt;deactivate&lt;/strong&gt;. All these action are currently implemented in a CLI interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Commands &lt;span class="k"&gt;for &lt;/span&gt;activating, validating, and deactivating licenses  

Usage:  
 devtui license &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;  

Available Commands:  
 activate    Activate a license  
 deactivate  Deactivate a license  
 validate    Validate a license  

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Activate
&lt;/h3&gt;

&lt;p&gt;But I expect that most users will probably invoke only one command - &lt;code&gt;activate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;devtui license activate --key=DEVTUI-2CA57A34-E191-4290-A394-XXXXXX&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I'm intentionally not using any ENV variables — this would require an additional library, and I don’t want users polluting their &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.bashrc&lt;/code&gt; with extra variables. &lt;/p&gt;

&lt;p&gt;After activation, the key and related info are stored in a &lt;code&gt;license.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;During activation, it’s possible to provide "conditions". In our case, we use the MAC address to associate the license with a specific machine. The licensing server enforces a maximum of 3 machines per license.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate
&lt;/h3&gt;

&lt;p&gt;This action uses the &lt;code&gt;license.json&lt;/code&gt; file created during activation. While the CLI command simply checks that the license is active, this validation logic is also embedded directly in the app.&lt;/p&gt;

&lt;p&gt;Here's a rough outline of the validation logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's checking that hash sum matches expectations&lt;/li&gt;
&lt;li&gt;If it doesn't match -&amp;gt; We re-validate license with server&lt;/li&gt;
&lt;li&gt;If it does match -&amp;gt; We check if it's time to check license with a server.&lt;/li&gt;
&lt;li&gt;During a license check with server, if MAC address is not similar to one we used during activation - validation will fail. This would prevent people from just moving &lt;code&gt;license.json&lt;/code&gt; file to another machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Software attempts to validate license with a server every week, this time frame is completely arbitrary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deactivate
&lt;/h3&gt;

&lt;p&gt;This also uses the &lt;code&gt;license.json&lt;/code&gt; file created during activation.&lt;/p&gt;

&lt;p&gt;While it's already possible to deactivate a license via the customer portal, having it available in the CLI felt important. Two use cases come to mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running the app in CI environments where machines constantly get recycled&lt;/li&gt;
&lt;li&gt;Switching from one machine to another&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Identifying a Machine
&lt;/h2&gt;

&lt;p&gt;To my knowledge, most software uniquely identify a machine by its MAC address. It's important to try and find the physical, manufacturer-assigned MAC address, use it during license activation, and later rely on it during validation.&lt;/p&gt;

&lt;p&gt;However, not all MAC addresses are created equal. There are also Locally Administered Addresses (LAAs), these are not the same because they could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually assigned by a network administrator&lt;/li&gt;
&lt;li&gt;Often used in virtual machines&lt;/li&gt;
&lt;li&gt;Common in network virtualization scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's possible to identify those by checking the second least significant bit of the first octet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the bit is 1, it's locally administered&lt;/li&gt;
&lt;li&gt;If it's 0, it's universally administered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;02:00:00:00:00:00 → locally administered&lt;/li&gt;
&lt;li&gt;00:1A:2B:3C:4D:5E → universally administered&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  License File
&lt;/h2&gt;

&lt;p&gt;We use the &lt;a href="//github.com/adrg/xdg"&gt;github.com/adrg/xdg&lt;/a&gt; library to determine where to store the license file. &lt;/p&gt;

&lt;p&gt;This library implements the &lt;a href=""&gt;XDG Base Directory and XDG User Directory specifications&lt;/a&gt;, offering a standard mechanism for storing application state, data, or configuration across multiple OSes—Windows, Linux, Plan 9, and macOS.&lt;/p&gt;

&lt;p&gt;In the app’s case, we rely on &lt;code&gt;XDG_DATA_HOME&lt;/code&gt;, or fallbacks as per OS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unix&lt;/th&gt;
&lt;th&gt;macOS&lt;/th&gt;
&lt;th&gt;Plan 9&lt;/th&gt;
&lt;th&gt;Windows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;~/.local/share&lt;/td&gt;
&lt;td&gt;~/Library/Application Support&lt;/td&gt;
&lt;td&gt;$home/lib&lt;/td&gt;
&lt;td&gt;LocalAppData&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example 'license.json':&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7394991a704a054096f4484d8a19f9ac66e3e8c98b68603652feadc785a364f2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"license_key_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEVTUI-2CA57A34-E191-4290-A394-XXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"activation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2ee71107-2ecb-4172-aff7-ceaa6b2f7cef"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"next_check_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-12T23:18:12.7724912+02:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"last_verified_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-05-05T23:18:12.772488825+02:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hash is generated from multiple values and is used to verify that the license data hasn't been tampered with. If the hash doesn't match, the app treats the license as invalid. &lt;/p&gt;

&lt;p&gt;I won’t go into the full details of what goes into the hash, but in general:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MAC address&lt;/li&gt;
&lt;li&gt;License key&lt;/li&gt;
&lt;li&gt;Activation time&lt;/li&gt;
&lt;li&gt;Next check timestamp&lt;/li&gt;
&lt;li&gt;A salt value (random string)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mechanism isn’t hacker-proof, but the effort required to crack it should outweigh the $40 one-time price tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final notes
&lt;/h2&gt;

&lt;p&gt;This is my first time implementing client-side license logic for an app, so I’ve probably missed some edge cases.  But if anyone has some feedback or recommendations, please reach out.&lt;/p&gt;

</description>
      <category>polar</category>
      <category>cli</category>
      <category>tui</category>
    </item>
    <item>
      <title>POSH 0.9: Experimental support for gems</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Sat, 14 Dec 2024 00:37:00 +0000</pubDate>
      <link>https://dev.to/skatkov/posh-09-experimental-support-for-gems-246g</link>
      <guid>https://dev.to/skatkov/posh-09-experimental-support-for-gems-246g</guid>
      <description>&lt;p&gt;One of the biggest releases in a while!&lt;/p&gt;

&lt;p&gt;The main theme for this release is &lt;strong&gt;experimental support for gem docsets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This required significant changes on all fronts - both back-end and client (TUI).&lt;/p&gt;

&lt;p&gt;There is now a “POSH builder” project that handles all the heavy lifting of building Markdown documentation. It’s currently a semi-automatic process that requires some manual setup. However, eventually, the entire process could be automated; I’ve already &lt;a href="https://hooks.poshtui.com/" rel="noopener noreferrer"&gt;managed to (web)hook into RubyGems&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most of the work has been with the TUI client.&lt;/p&gt;

&lt;p&gt;The TUI is able to parse the &lt;code&gt;Gemfile.lock&lt;/code&gt; file, download all documentation it can find, and present it through a TUI view. While the flow of this entire process is not yet properly defined, the features for each step seem to be working.&lt;/p&gt;

&lt;p&gt;Here’s a quick rundown of new features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;posh docsets&lt;/code&gt; to download all available docsets.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;posh docsets --clear&lt;/code&gt; will remove all docsets from the system.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;posh gemfile&lt;/code&gt; to view project gems with the ability to search and pick a docset for review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The client now stores a local cache to avoid redownloading docsets multiple times.&lt;/li&gt;
&lt;li&gt;There are many stability improvements to the client itself and better error handling.&lt;/li&gt;
&lt;li&gt;Underlying libraries have been updated; the biggest win here is that the markdown renderer has received numerous improvements. This should generally help to present documentation better.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>New version of poshtui.com live</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Mon, 09 Dec 2024 16:04:00 +0000</pubDate>
      <link>https://dev.to/skatkov/new-version-of-poshtuicom-live-4f11</link>
      <guid>https://dev.to/skatkov/new-version-of-poshtuicom-live-4f11</guid>
      <description>&lt;p&gt;POSH TUI is preparing for a major release, but before announcing that, I gave the homepage a fresh look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://poshtui.com/" rel="noopener noreferrer"&gt;https://poshtui.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TUI’s have been around for a long time, so I went with a timeless brutalism design. This is my first attempt at expressing the type of design that I’m looking forward to bringing to the web.&lt;/p&gt;

&lt;p&gt;The website now features a proper landing page, changelog, and feedback portal. Once the core features of this tool are completed, user feedback will be guiding the priorities for this project.&lt;/p&gt;

&lt;p&gt;There is still some work needed on this website, in short term I’ll add pricing and FAQ sections. But this website is already better than the previous one, so I decided to hit that publish button.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Marrying Tailwind with Jekyll</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Mon, 30 Sep 2024 21:04:47 +0000</pubDate>
      <link>https://dev.to/skatkov/marrying-tailwind-with-jekyll-5c72</link>
      <guid>https://dev.to/skatkov/marrying-tailwind-with-jekyll-5c72</guid>
      <description>&lt;p&gt;Ruby has more than one beloved framework - there's also &lt;a href="https://jekyllrb.com/" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Jekyll is a simplistic framework for static websites that originally sparked the static website and JAMstack movements. While there are many similar frameworks with more features, Jekyll remains one of the simplest on the market. It has been somewhat forgotten and hasn't evolved much lately, to the point where some people decided to take matters into their own hands and fork this framework into something called &lt;a href="https://github.com/bridgetownrb/bridgetown" rel="noopener noreferrer"&gt;Bridgetown&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Surprisingly, in many projects, I don't really need more than what Jekyll can offer. So, I remain a loyal user. Until recently, I didn't have much to complain about, not until I decided to rebuild &lt;a href="https://poshtui.com" rel="noopener noreferrer"&gt;poshtui.com&lt;/a&gt; with &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;. The only way to add &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; and &lt;a href="https://heroicons.com/" rel="noopener noreferrer"&gt;Heroicons&lt;/a&gt; was through a JavaScript bundler, and I'm no longer used to that approach.&lt;/p&gt;

&lt;p&gt;As it sometimes happens in the Open Source world - the stars aligned in the right way. I stumbled upon work by &lt;a href="https://x.com/crbelaus" rel="noopener noreferrer"&gt;@crbelaus&lt;/a&gt; on &lt;a href="https://github.com/crbelaus/jekyll-tailwind" rel="noopener noreferrer"&gt;jekyll-tailwind&lt;/a&gt;. He demonstrated that it's possible to integrate both, but his solution was lacking. I then found work by &lt;a href="https://x.com/flavorjones" rel="noopener noreferrer"&gt;@flavorjones&lt;/a&gt; who extracted the &lt;a href="https://github.com/flavorjones/tailwindcss-ruby" rel="noopener noreferrer"&gt;tailwindcss-ruby gem&lt;/a&gt; from the Rails core gem.&lt;/p&gt;

&lt;p&gt;To keep things short, over the next couple of weeks, I worked to improve the integration of Tailwind and Heroicons with Jekyll. An example of this work can be found in the &lt;a href="https://github.com/skatkov/jekyll-tailwind-cli-template" rel="noopener noreferrer"&gt;jekyll-tailwind-cli-template&lt;/a&gt;. The current setup experience with &lt;code&gt;jekyll-heroicons&lt;/code&gt; and &lt;code&gt;jekyll-tailwind&lt;/code&gt; is not exactly a walk in the park, but it's much easier than working with JavaScript bundlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  jekyll-tailwind
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/flavorjones/tailwindcss-ruby" rel="noopener noreferrer"&gt;tailwindcss-ruby&lt;/a&gt; will be used in Rails, so this gem now depends on it as well.&lt;/p&gt;

&lt;p&gt;This work brought everything you would expect from Tailwind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minification&lt;/li&gt;
&lt;li&gt;PostCSS support&lt;/li&gt;
&lt;li&gt;All the tweaking of tailwind.config.js&lt;/li&gt;
&lt;li&gt;Ability to provide input/output files&lt;/li&gt;
&lt;li&gt;People can move to Tailwind v4 on their own accord&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://x.com/flavorjones" rel="noopener noreferrer"&gt;@flavorjones&lt;/a&gt; did a wonderful job here; it was a breeze to write the integration! &lt;a href="https://x.com/crbelaus" rel="noopener noreferrer"&gt;@crbelaus&lt;/a&gt; was a great collaborator, and he offered me to step up as a maintainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  jekyll-heroicons
&lt;/h2&gt;

&lt;p&gt;Besides the obvious way to distribute SVGs without a JavaScript bundler, the gem offers small quality of life improvements. Any icon can be used just by typing a name:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{% heroicon bell %}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can define a default variant in _config.yml, but you can also overwrite it on a per-icon basis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroicons:
  variant: 'solid'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's possible to define default classes that will be applied to all icons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;heroicons:
  default_class: {
    solid: "size-6",
    outline: "size-6",
    mini: "size-5",
    micro: "size-4",
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it's also possible to turn that off, provide your own classes, or any other tags for the SVG element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Disable default_classes
{% heroicon bell disable_default_class: true %}
# Provide additional classes
{% heroicon bell class: "text-red-500" %}
# Provide any other attribute for svg html element
{% heroicon bell class: "text-red-500" aria-hidden: true height:32 aria-label:hi %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no need to copy SVGs one by one or reach out to a JavaScript bundler!&lt;/p&gt;

&lt;h2&gt;
  
  
  Ending notes
&lt;/h2&gt;

&lt;p&gt;Jekyll might have a chance for a revival in light of recent trends in the Ruby ecosystem. Ruby developers are eagerly looking for ways to simplify and condense complexity. And now, there's a simpler way to use Tailwind CSS with this amazing framework. I hope you'll find it useful.&lt;/p&gt;

</description>
      <category>jekyll</category>
      <category>tailwindcss</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Munster - Webhooks processing engine for Rails</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Tue, 18 Jun 2024 16:34:39 +0000</pubDate>
      <link>https://dev.to/skatkov/munster-webhooks-processing-engine-for-rails-2l10</link>
      <guid>https://dev.to/skatkov/munster-webhooks-processing-engine-for-rails-2l10</guid>
      <description>&lt;p&gt;By the time of writing this article, I had already written webhook processing logic at least 10 times for different companies and clients. Together with &lt;a href="https://blog.julik.nl/"&gt;Julik&lt;/a&gt; we have implemented one recently at our &lt;a href="https://cheddar.me"&gt;current place of employment&lt;/a&gt;. And guess what? Once a second service had to be built, it needed to accept webhooks too.&lt;/p&gt;

&lt;p&gt;Our combined experience in the ingestion of webhooks had already produced a reasonable generic solution. Should we just copy some files over to a microservice and duplicate that code? Nah, let's save the world from wasting those countless hours of re-implementing webhooks over and over again!  This darn well could be a gem!&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter "Munster"
&lt;/h2&gt;

&lt;p&gt;Munster is a webhooks processing engine for Rails. And you heard that right: it's only accepting and processing, not sending.&lt;/p&gt;

&lt;p&gt;Services that send webhooks would love it if you accepted those as fast as possible and responded with a 200 status code. Pretty much always, actually - because if your service refuses to accept the webhooks they send you they will likely stop retrying. Important business events could then get lost, you would need to examine the retry policies of every webhook sender, etc.&lt;/p&gt;

&lt;p&gt;And if you didn't manage to receive a webhook correctly, it is usually a hassle to ask the sending service to re-deliver the missed webhooks to you in bulk. Some (like Stripe) provide facilities to replay their event feed in a "pull" manner, but most do not.&lt;/p&gt;

&lt;p&gt;How to make that happen well? The answer is pretty simple: do not process the webhook inline. Verify it is coming from the sender (most good webhooks use some form of signature that you can verify using a shared secret or a public key), and spool the webhook for processing using your background jobs. Processing webhooks asynchronously has many other advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Equally manage load on servers.&lt;/li&gt;
&lt;li&gt;Background process is not subject to any request timeouts.&lt;/li&gt;
&lt;li&gt;If processing fails, we will have a webhook safely stored in our database for later re-processing or analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Munster is an engine which will provide you the following facilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small abstraction for building webhook receiving &lt;em&gt;handlers.&lt;/em&gt; A handler is a small object with just a handful of methods that you need to implement - normally those would be &lt;code&gt;valid?&lt;/code&gt; and &lt;code&gt;process&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A Rails engine that can be mounted into your application and handles webhooks from multiple senders together - every webhook sender would get its own &lt;em&gt;handler&lt;/em&gt; definition. So you would have a handler for Stripe, a handler for Revolut, a handler for Github - and any other services you might want to receive webhooks from&lt;/li&gt;
&lt;li&gt;A background job class which calls &lt;code&gt;process&lt;/code&gt; asynchronously, from your job queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;As with any other gem, you'd first want to add &lt;code&gt;gem 'munster'&lt;/code&gt; into a &lt;code&gt;Gemfile&lt;/code&gt; and run &lt;code&gt;bundle install&lt;/code&gt;. This would add a generator task to a Rails project, so run &lt;code&gt;rails g munster:install&lt;/code&gt;. Then run &lt;code&gt;rails db:migrate&lt;/code&gt; so that the table used for received webhooks gets created.&lt;/p&gt;

&lt;p&gt;It will create the required migration and an initializer file at &lt;code&gt;app/initializers/munster.rb&lt;/code&gt;. This initializer would expect you to define at least one &lt;code&gt;active_handlers&lt;/code&gt; hash for it. I will quickly run you through the process and give you an example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mounting
&lt;/h3&gt;

&lt;p&gt;Munster is an engine and a Rack app,  so it can be mounted in your Rails routes file. Inside &lt;code&gt;config/routes.rb&lt;/code&gt; you can mount a webhook engine on a subdomain (like &lt;code&gt;webhooks.yourdomain.com/:service_id&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s2"&gt;"webhooks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;constraints: &lt;/span&gt;&lt;span class="s2"&gt;"webhooks.yourdomain.com"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Munster&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;at: &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on the main domain (&lt;code&gt;yourdomain.com/webhooks/:service_id&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mount Munster::Engine =&amp;gt; "/webhooks"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Having a separate subdomain for receiving webhooks can be useful if you have very specific security requirements, or if you would like to have a separate load balancer fronting your webhook receiving endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining a Handler
&lt;/h3&gt;

&lt;p&gt;The next step is to define a webhook handler. For the sake of an example, let's create a handler for Customer.io at &lt;code&gt;app/webhooks/customer_io_handler.rb&lt;/code&gt;. The handler will take care of handling two specific metrics from Customer.io - &lt;code&gt;subscribed&lt;/code&gt; and &lt;code&gt;unsubscribed&lt;/code&gt;. We want to store a local value per user called "subscribed" – once a user unsubscribes using customer.io, we want to record this information in our app database. Same for when a user subscribes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Webhooks::CustomerIoHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Munster&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseHandler&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processing!&lt;/span&gt;

      &lt;span class="c1"&gt;# The webhook body gets stored as bytes, so senders may deliver&lt;/span&gt;
      &lt;span class="c1"&gt;# you binary data on the endpoint - it does not have to be JSON&lt;/span&gt;
      &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;symbolize_names: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:metric&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"subscribed"&lt;/span&gt;
        &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:customer_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;subscribed: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processed!&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"unsubscribed"&lt;/span&gt;
        &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;subscribed: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processed!&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipped!&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
      &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error!&lt;/span&gt;

      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_event_id_from_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_dispatch_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_dispatch_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify that request is actually comming from customer.io&lt;/span&gt;
    &lt;span class="c1"&gt;# @see https://customer.io/docs/api/webhooks/#section/Securely-Verifying-Requests&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valid?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_dispatch_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;xcio_signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action_dispatch_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_X_CIO_SIGNATURE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;xcio_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action_dispatch_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_X_CIO_TIMESTAMP"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;request_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action_dispatch_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;
      &lt;span class="n"&gt;string_to_sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"v0:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;xcio_timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

      &lt;span class="n"&gt;hmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HMAC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SHA256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_io_webhook_signing_key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string_to_sign&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;secure_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xcio_signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're rewriting three methods from &lt;a href="https://github.com/cheddar-me/munster/blob/main/lib/munster/base_handler.rb"&gt;BaseHandler&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;valid?&lt;/code&gt; method verifies that the webhook indeed comes from customer.io. This method runs inline, before we persist the webhook.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;process&lt;/code&gt; method defines how we want to process data&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;extract_event_id_from_request&lt;/code&gt; method defines how to extract an ID from a webhook, and by default it will generate a random UUID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;extract_event_id_from_request&lt;/code&gt; is a very important feature. A lot of webhook senders send you unique webhooks, but those webhooks may arrive out of order, or arrive twice. Networks being unreliable, the retries being too aggressive at the sender side, you name it. Munster will use this event ID to deduplicate your webhook - if you receive the same data more than once, just one webhook will be persisted and processed. This will also protect your infrastructure if, for some reason, the webhook sender happens to DoS you with repeated deliveries.&lt;/p&gt;

&lt;p&gt;There are more methods that could be redefined and tweaked, but these three are most commonly used.&lt;/p&gt;

&lt;p&gt;And lastly we need to mount our handler. We use the "service ID", which will also be the URL path component for your webhook. For our handler, we will use "customer-io" (our URL to paste into the Customer.io configuration will thus be &lt;code&gt;https://your-app.example.com/webhooks/customer-io&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'../../app/webhooks/customer_id_handler.rb'&lt;/span&gt;

&lt;span class="no"&gt;Munster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"customer-io"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Webhooks&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CustomerIoHandler&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final note
&lt;/h2&gt;

&lt;p&gt;We went through the basic webhook endpoint setup with Munster, but it offers a bit more than that. This little framework is especially beneficial in cases where you have more than one webhook endpoint. Once you set it up in your project , it's just a matter of defining a single handler to accept new types of webhooks.&lt;/p&gt;

&lt;p&gt;The core of this is already battle-tested at &lt;a href="https://www.cheddar.me"&gt;cheddar.me&lt;/a&gt;, but we're still working on fleshing out the details and would love your feedback! You can find Munster at &lt;a href="https://github.com/cheddar-me/munster"&gt;https://github.com/cheddar-me/munster&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>webhooks</category>
      <category>ruby</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Interview with Greg Molnar - Rails developer and penetration tester</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Wed, 05 Jul 2023 09:19:04 +0000</pubDate>
      <link>https://dev.to/skatkov/interview-with-greg-molnar-rails-developer-and-penetration-tester-3aj5</link>
      <guid>https://dev.to/skatkov/interview-with-greg-molnar-rails-developer-and-penetration-tester-3aj5</guid>
      <description>&lt;p&gt;One of the sections in upcoming &lt;a href="https://lintingruby.com"&gt;Linting Ruby&lt;/a&gt; book is about automating security checks in ruby. I never specialized on security in Rails application, nor did Lucian. We didn't felt qualified to give our opinions as general advice on such an important topic. So, we decided to conduct a thorough research and reach out to people who know better.&lt;/p&gt;

&lt;p&gt;So we present you interview with &lt;a href="https://twitter.com/GregMolnar"&gt;Greg Molnar&lt;/a&gt;, who is a Rails developer for 13 years and &lt;a href="https://www.offensive-security.com/information-security-certifications/oscp-offensive-security-certified-professional/"&gt;OSCP-certified&lt;/a&gt; penetration tester. You might know him as an author of &lt;a href="https://github.com/gregmolnar/spektr"&gt;Spektr&lt;/a&gt; a static code analysis tool that finds potential vulnerabilities in Rails and  work in progress book titled &lt;a href="https://github.com/gregmolnar/spektr"&gt;"Secure Code Review for Rails developers"&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stas: How did you get interested in security?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: I ended up working for a company that did penetration testing and security consultancies. They hired me as a  Ruby on Rails developer to work on their internal applications.&lt;br&gt;
They were always telling me about all the cool things they do and got me infected with all of that. I wanted to learn more about the world of InfoSec. Unfortunately, I had to leave that company and England, but I took a penetration testing course that they recommended to me.&lt;/p&gt;

&lt;p&gt;This course is supposedly one of the hardest out there, it's called Offensive Security Certified Professional or OSCP for short. It's a very hands-on penetration testing course. For the exam, they give you five IP addresses and 48 hours. Your task is to find vulnerabilities, and get root access on all of them, and write a report.&lt;br&gt;
And since then, I have been splitting my time between doing development and security work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: Can you tell us more, what your security work looks like... Do you help with Rails code reviews or you try to break into their systems?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;:I do penetration tests, not only for Ruby on Rails projects. I don't mind the underlying technology actually.&lt;br&gt;
Most of my work is for companies with a requirement of being PCI compliant. Part of that is to have yearly penetration test on their application. For instance if a company works in the financial industry, their customers and partners likely require them to be PCI compliant due to the sensitive data they handle. But I also work for companies getting a test done to just cover their bases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: Based on your experience which are the most common problems? Most common security issues? Is it on application level or because infrastructure is misconfigured?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: Assessing infrastructure testing is not really something that I typically do. In fact, I can't remember when I last did it because usually, these tasks are separated in companies. There's the development team which handles the web penetration test - the application's test. Then, there's the operations team, which is responsible for their own security tests and they hire their own consultants.&lt;/p&gt;

&lt;p&gt;The most common vulnerability, I believe, is still Cross-Site Scripting(XSS). XSS is very frequent, but often, I find cases where proper configuration via X-XSS-Protection&lt;br&gt;
 and alike headers prevent full exploitation, even when developers make mistakes. So, vulnerability is there, but nobody can exploit it. I can run the script, but it won't execute - an exception is thrown in the browser console saying you are not allowed to do this.&lt;br&gt;
 Besides that, authorization issues are the most common ones. When a user can access or do things they shouldn't be permitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: There's a gem called 'bundle-audit' that checks if any of your dependencies have known security issues. However, we're seing more and more attacks these days involving third-party libraries. Is it enough to rely only on 'bundle-audit' to secure your third-party libraries?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: 'bundle-audit' works same way as other alternatives as far as I know, so relying on that should be enough I think. But if someone hijacks a ruby gem and publishes a version with a malware, nothing can really save you except your own manual due diligence.&lt;br&gt;
I recommend to always cross-check the repository and rubygems for updates when you upgrade a gem. When you get an new version of a gem from rubygems, go to GitHub to search for the same tag. If you can't find that tag on GitHub, you have to ask why is that. When I also always the changelog on GitHub if there is one. I consider it a red flag, if I can't find the tag or a changelog for the version I am getting from rubygems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: Do you use dependabot?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: No, to be honest, never particularly liked this tool, even before GitHub acquired it. Now, I don't favor GitHub either due to the Microsoft acquisition.&lt;br&gt;
I strive to maintain my independence and avoid reliance on a tool unique to GitHub. For instance, with bundle-audit, I can take my repository anywhere and still use that tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: So you don't stay on a bleeding edge of gems and probably have some lag time before you'll update. Since there is a need to check every update for every gem?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: If there's a security update, and it is likely exploitable in the apps I work on, I update immediately. As for the regular gem updates, I batch them and do upgrade days when I upgrade as many as I can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: When you're developing Rails applications, as someone who is so security-minded, do you use any tools to help prevent security issues?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: Yes, 'bundle-audit' and 'Brakeman', which are very well-known and widely used.&lt;br&gt;
I actually created my own gem, similar to 'Brakeman'. Mainly because 'Brakeman' was acquired a few years back and subsequently changed their license. Under the new license, you can use it for free with your own application, but once you start running 'Brakeman' on someone else's application who is paying you, that infringes on their terms of use.&lt;br&gt;
My gem, &lt;a href="https://github.com/gregmolnar/spektr"&gt;Spektr&lt;/a&gt;, is somewhat similar, bit it built with a different parsing library and it has a different license, so penetration testers can run it for free during assignments. But the core idea is the same as Brakeman's. It might report a few false positives, but even those might give ideas for patterns to look into during a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: How about Rubocop? Rubocop had cops that are security related.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: Yes, RuboCop does have security features, but I think they overlap with Brakeman, so I don't really use them. I use RuboCop for style enforcement, but not for the security aspects. Perhaps I should take a closer look and see how it compares to the other tools.&lt;br&gt;
I believe that Brakeman and bundle-audit pretty much cover everything that can be detected automatically. However, there are other security issues, particularly those of logical nature, that cannot be flagged automatically. If you forget enforce authorization for an endpoint - no automatic tool can point it out. For that, I often recommend writing tests for every different role, to ensure they have access to only the things they're supposed to.&lt;br&gt;
Use a whitelist, not a blacklist - if you add something new, it's blocked by default and can only be enabled intentionally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: It seems like we can automate some aspects of security checks, correct? But it also seems like this portion is relatively limited, implying a significant amount of manual work is still necessary.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: I believe you can automate about half of security checks, but the other half still requires vigilance. You still need to write tests and make sure you're implementing all authorization and authentication rules properly, and continually check that they are functioning as they should.&lt;br&gt;
Certainly, you can - or rather, should - automate some portion, primarily because it's a straightforward process. Why wouldn't you take advantage of it if it's that easy and offers a head start against potential attacks?&lt;/p&gt;

&lt;h2&gt;
  
  
  Stas: In your experience, you mentioned having worked with PHP and other stacks. In terms of security, how do Rails developers compare to other programmers? Does Rails significantly enhance security by default? Are Rails developers generally more knowledgeable about security than PHP developers, based on your experiences?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Greg&lt;/strong&gt;: Rails' default security settings protect against a lot of common mistakes. It's hard to create an XSS vulnerability because everything is escaped by default, with only a few exceptions. You need to explicitly state that a string is HTML safe; otherwise, it's automatically treated as unsafe. This makes life much easier.&lt;/p&gt;

&lt;p&gt;90% of the ActiveRecord API is immune to SQL injection, which means that you don’t need extensive security knowledge to maintain a safe environment.&lt;/p&gt;

&lt;p&gt;I believe, however, that this could lead to people not taking security as seriously as they should, they think Rails takes over that responsibility, but that's impossible, there is no way to make a functioning framework without letting people to shoot themself in the leg if they want to.&lt;/p&gt;

&lt;p&gt;When I'm writing code, I'm solving a problem. I don't think about security as a first class citizen. I always think about, okay, let's solve this problem in the most efficient way. And then I look at it and think about the security aspect. But it's only because I care about security, but a lot of people miss that second step.&lt;/p&gt;

&lt;p&gt;Another aspect to consider is for instance, with PHP, if I discover a vulnerability in a PHP application, it is relatively easy to gain access to the entire server or infrastructure, because it is running on a server with a bunch of tools installed and those usually help to open a remote shell and elevate privileges. This is much easier than finding a vulnerability in, say, a Rails app running on Heroku in a container.&lt;/p&gt;




&lt;p&gt;If you're interested in linting and ruby, you might want to follow along as we write our book on &lt;a href="https://lintingruby.com"&gt;https://lintingruby.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ruby</category>
      <category>linting</category>
      <category>interview</category>
    </item>
    <item>
      <title>Week 6: search index and distribution</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Fri, 28 Oct 2022 19:53:03 +0000</pubDate>
      <link>https://dev.to/skatkov/week-6-search-index-and-distribution-4oc1</link>
      <guid>https://dev.to/skatkov/week-6-search-index-and-distribution-4oc1</guid>
      <description>&lt;p&gt;This is a third progress report for a command line tool called "Posh TUI". It's an API documentation browser —like &lt;a href="https://kapeli.com/dash"&gt;Dash&lt;/a&gt;, but for your console.&lt;/p&gt;

&lt;p&gt;Software is not yeat available, but there is a &lt;a href="https://skatkov.gumroad.com/l/pxbxx"&gt;limited pre-sale for a lifetime license&lt;/a&gt;. It includes all future updates for free, but also I'll be closely listening to feedback and will be building future roadmap based on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Previously I have explained, that project consists of two big parts that need to work together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation generation and hosting&lt;/li&gt;
&lt;li&gt;TUI app that would allow to review/search documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since I'm unable to render html in console, documentation have to come in form of Markdown. Two weeks ago, I embarked on a journey to build RDoc plugin to generate markdown documentation for Ruby and libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progress report
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RDoc-markdown gem
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://asciinema.org/a/1dtu94Spv0UfylwTRJksyk7Nx"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OlUI6HKw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://asciinema.org/a/1dtu94Spv0UfylwTRJksyk7Nx.png" alt="" width="880" height="266"&gt;&lt;/a&gt; Building markdown documentation and previewing it!&lt;/p&gt;

&lt;p&gt;I'm excited to report, that I completed rdoc-markdown gem. Version 0.3 of this library produces not only markdown files, but also search index file. It probably still has some quirks that would need fixing, but this is already enough for me to move forward and build a TUI client.&lt;/p&gt;

&lt;p&gt;Since plugin was built as open source, I made sure that it's easy to approach for collaborators. Don't be shy and take a peak! I'm always open to feedback.&lt;/p&gt;

&lt;p&gt;While working on TUI client, I'll be showing off this plugin in different ruby communities. This may yield some great improvements and feedback or will just pass unnoticed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docsets delivery
&lt;/h3&gt;

&lt;p&gt;Another part of puzzle is how to deliver docsets, to smplify things I decided to just distribute all docsets as one archive. Docsets includes only ruby language and Ruby on Rails frameworks that did not reach end of life. In terms of size, that's not a big package to download.&lt;/p&gt;

&lt;p&gt;This delivery mechanism will not scale well with more docsets. But I guess that will be a good problem to have later. For now, I can just skip all of it.&lt;/p&gt;

&lt;p&gt;I have packaged 3 latest Rails release and 3 latest Ruby releases. And currently using those for local tests.&lt;/p&gt;

&lt;p&gt;I did &lt;a href="https://aws.amazon.com/free/"&gt;apply for Free AWS credits&lt;/a&gt; as a startup founder and got approved within 2 days. Now I'll have credits to distribute these free of charge for myself. Thank you, Amazon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;At this stage, most of the "back office" things are finished, and I'm ready to work on a client facing TUI app. My goal for next 2 weeks would be to deliver first prototype to existing customers.&lt;/p&gt;

&lt;p&gt;It might not do much, but it should already be possible to play around with it and have a reliable delivery mechanism where people can validate their licenses and update their software.&lt;/p&gt;

&lt;p&gt;Off I go to work on most exciting part of this project!&lt;/p&gt;

</description>
      <category>rdoc</category>
      <category>sqlite</category>
      <category>markdown</category>
      <category>aws</category>
    </item>
    <item>
      <title>Week 4: week 4 progress update.</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Mon, 17 Oct 2022 09:20:22 +0000</pubDate>
      <link>https://dev.to/skatkov/posh-tui-week-4-progress-update-3e19</link>
      <guid>https://dev.to/skatkov/posh-tui-week-4-progress-update-3e19</guid>
      <description>&lt;p&gt;This is a second progress report for a command line tool called "Posh TUI". It's an API documentation browser for ruby developers.&lt;/p&gt;

&lt;p&gt;My name is Stanislav Katkov. I got really frustrated that there are no viable alternatives for &lt;a href="https://kapeli.com/dash"&gt;Dash&lt;/a&gt;, so embarked on a journey to build one. If you are not using Mac/OSX or prefer command line tools to GUI tools and share a similar frustration—you might want to &lt;a href="https://skatkov.gumroad.com/l/pxbxx"&gt;buy a lifetime license&lt;/a&gt;. This will guarantee that you'll get any updates of app for free in future and your feedback will be driving roadmap for this tool going forward.&lt;/p&gt;

&lt;p&gt;This deal is limited to 20 licenses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;This project is basically split into two major components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI/TUI app to review documentation&lt;/li&gt;
&lt;li&gt;framework to generate and host required documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initially, attempted skipping documentation part and tried to build CLI/TUI app based on &lt;a href="https://github.com/freeCodeCamp/devdocs"&gt;devdocs&lt;/a&gt; documentation.  Ran into issues converting html to markdown on a fly and couldn't proceed with implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.poshtui.com/quest-to-generate-documentation-in-markdown/"&gt;I did research about alternatives&lt;/a&gt; and decided to proceed with RDoc integration instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progress report
&lt;/h2&gt;

&lt;p&gt;In these two weeks, I had to move my entire family from Thailand back home to the Netherlands. After two months of absence from home, we had a huge layer of dust waiting for us to clean. Most of my spare time in weekends was spent cleaning everything up.&lt;/p&gt;

&lt;p&gt;But there is still some progress to report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rdoc-markdown gem
&lt;/h2&gt;

&lt;p&gt;Started out, by just forking rdoc repo—effectively creating a fork with markdown generator. But that's a bad idea, easier to write a plugin!&lt;/p&gt;

&lt;p&gt;There area only couple of things to remember while writing rdoc plugin. Rdoc will look for &lt;em&gt;rdoc/discover.rb&lt;/em&gt; files in installed gems, this file is used to load entire plugin.&lt;/p&gt;

&lt;p&gt;Testing a rdoc plugin is way trickier, though. How would you even test it locally? Having a robust testsuit is close to impossible to cover all possible scenarios. Which flavor of Markdown to support?&lt;/p&gt;

&lt;p&gt;I have settled with &lt;a href="https://github.github.com/gfm/"&gt;Github Flavored Markdown&lt;/a&gt; as a standard. But even with this well-defined standard, it's close to impossible to validate programmatically.&lt;/p&gt;

&lt;p&gt;Entire testing is being done manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build and install new versions of a gem regularly&lt;/li&gt;
&lt;li&gt;Store source code of ruby and Ruby on Rails project&lt;/li&gt;
&lt;li&gt;Generate documentation with &lt;em&gt;--markup=markdown --debug&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Compare with HTML version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it is a manual tedious process, time-consuming, and fragile. I probably typed these commands a couple of hundred times at this point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;gem build rdoc-markdown.gemspec&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;gem uninstall rdoc-markdown&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;gem install /rdoc-markdown/rdoc-markdown-.gem&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gem is already open-sourced at &lt;a href="https://github.com/skatkov/rdoc-markdown"&gt;github/skatkov/rdoc-markdown&lt;/a&gt;. It currently generates readable documentation in Markdown, but there are still some quirks in rendering I stumble upon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Wrapping up &lt;a href="https://github.com/skatkov/rdoc-markdown"&gt;rdoc-markdown&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Address all the rendering quirks that I'll find during testing&lt;/li&gt;
&lt;li&gt;Produce a SQLite database with index of entire content&lt;/li&gt;
&lt;li&gt;Post to various ruby related communities to get initial feedback.&lt;/li&gt;
&lt;li&gt;Produce documentation for supported versions of Ruby and Ruby on Rails gems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After move on to building a CLI app again (looking forward to coding some GO!). My initial goal is to integrate licenses from gumroad and to be able to analyze Gemfile (so I can derive ruby version and rails version from it).&lt;/p&gt;

&lt;p&gt;That's all for today, folks.&lt;/p&gt;

</description>
      <category>rdoc</category>
      <category>ruby</category>
      <category>gem</category>
      <category>rails</category>
    </item>
    <item>
      <title>Week 2: week 2 progress: building API documentation browser for command line</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Sat, 15 Oct 2022 11:08:27 +0000</pubDate>
      <link>https://dev.to/skatkov/buildinpublic-issue-1-building-api-documentation-browser-for-command-line-413g</link>
      <guid>https://dev.to/skatkov/buildinpublic-issue-1-building-api-documentation-browser-for-command-line-413g</guid>
      <description>&lt;p&gt;Hello everyone,&lt;/p&gt;

&lt;p&gt;This is my first installment of development journal for Posh TUI app. I'll do everything possible to make such notes a bi-weekly occurrence. It also helps me to be accountable and to structure my thoughts to ensure that everything is on a right track.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do I have so far?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HmQdUpiJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2rqr83fvaf7s32yqdx3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HmQdUpiJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2rqr83fvaf7s32yqdx3u.png" alt="Image description" width="880" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I already have a first prototype on my local computer, that takes HTML documentation, cleans it up and converts to markdown on a fly.  Why do I go through all this trouble to have a Markdown document? Well, standard terminal can't render HTML properly, it's mostly likely to show you plain-text content of HTML file.&lt;/p&gt;

&lt;p&gt;There is a way to switch terminal renderer, but that would make it inaccessible to a lot of people -- only to major geeks. So switching terminal renderer is not really a viable option either.&lt;/p&gt;

&lt;p&gt;My first prototype offers a nice way to review documentation in terminal like a book. But it comes with a bunch of serious downsides, that doesn't allow me to use this approach for a developer documentation browser. Namely:&lt;br&gt;
It takes a while to parse HTML and bigger pages takes noticeable time to load&lt;/p&gt;

&lt;p&gt;I can't really build a search feature on top of that. I need to know exactly which row to scroll down to, but if we convert HTML to markdown, number of rows changes and search throws us to an entirely different section.&lt;/p&gt;

&lt;p&gt;So, there is no way around this, I need to generate markdown and work with Markdown, no HTML. And this initial prototype will be scrapped and needs to rebuilt from ground up.&lt;/p&gt;

&lt;p&gt;But first, we must get that documentation reliably converted into markdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quest to render markdown documentation
&lt;/h2&gt;

&lt;p&gt;The most important step, to get this project off the ground, is to have a way to generate markdown documentation (or plain text) so we can render it in terminal. If I do not figure this out, there is not much else I can do. &lt;/p&gt;

&lt;p&gt;My first assumption was, that I should be able to generate markdown from the source. Same ruby and rails does now, but only tweaking a couple of parameters to generate .md files instead. &lt;a href="https://yardoc.org/"&gt;YARD&lt;/a&gt; is being used for that and it supports any markup rdoc or yard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Yard
&lt;/h3&gt;

&lt;p&gt;While on tin can it says, that it supports markdown, unfortunately output is only html. Played around with multiple parameters and it doesn't affect the output that much.&lt;/p&gt;

&lt;p&gt;While asking around, I've been meet with people being completely baffled by my request. "You still need to render markdown to something, and it will probably be HTML", was most common sentiment I've got.&lt;/p&gt;

&lt;p&gt;At this stage I understood, that what seemed like an easy-peasy task, might not be straight forward as I assumed.&lt;/p&gt;

&lt;h3&gt;
  
  
  RDoc
&lt;/h3&gt;

&lt;p&gt;RDoc doesn't really claim that it can produce anything other than HTML for documentation. But &lt;a href="https://rubygems.org/search?query=rdoc"&gt;multiple gems have proved otherwise&lt;/a&gt;, there are libraries that could convert rdoc to PDF and if you study RDoc source code closely enough -- you'll find that it can spit out JSON blob of entire content. That json blob could easily be transformed to Markdown and can easily be used to create an index of entire content (for search and navigation section).&lt;/p&gt;

&lt;p&gt;It seems, that most of the ruby itself, rails and all other gems I've stumbled upon work with rdoc. Even &lt;a href="https://kapeli.com/dash"&gt;DASH&lt;/a&gt; clearly indicates, that all gem documentation that they store are based of RDoc. &lt;/p&gt;

&lt;p&gt;So after a lot of studies on a matter, I've decided to write a generator for RDoc that would spit out markdown + index (as sqlite database) for me. And I'm planning to open-source that as a good netizen.&lt;/p&gt;

&lt;p&gt;I do have a bit of paranoia, though. What if things are not as easy as they seem again? That lead to research an alternative approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pandoc
&lt;/h3&gt;

&lt;p&gt;Pandoc is a very popular open-source document converter.&lt;/p&gt;

&lt;p&gt;I did a quick test trying to convert existing ruby documentation to Markdown, and it worked. But every document contained different artifacts (like issues with links, random HTML tags or whole sections that are not even needed) that required tweaking this entire process.&lt;/p&gt;

&lt;p&gt;Pandoc offers a way to write filter to deal with that, but I need to use Lua for that! I kinda decided against it. With a wrapper like &lt;a href="https://heerdebeer.org/Software/markdown/pandocomatic/"&gt;pandocomatic&lt;/a&gt;, it is still a workable solution. But I would rather leave it in case rdoc-Markdown idea will not work out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;I initially focused on building an CLI app, that can render markdown documentation and present a list of existing documentation on a system. The latter, doesn't seem useful, but markdown presenter might need to wait until Markdown is actually being properly generated.&lt;/p&gt;

&lt;p&gt;My next steps would be to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create rdoc-markdown generator and open source it.&lt;/li&gt;
&lt;li&gt;Create sqlite-index-builder that works with rdoc and existing markdown (and not open source it, because it's hard to make generic implementation)&lt;/li&gt;
&lt;li&gt;Start implementing basic CLI app -- it should validate licenses from gumroad and preferable be able to parse Gemfile (to understand ruby and gem versions in use)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's all for now, folks!&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>ruby</category>
      <category>tui</category>
      <category>docs</category>
    </item>
    <item>
      <title>Learning Go, was it worth it?</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Tue, 23 Aug 2022 14:43:07 +0000</pubDate>
      <link>https://dev.to/skatkov/learning-go-was-it-worth-it-7i2</link>
      <guid>https://dev.to/skatkov/learning-go-was-it-worth-it-7i2</guid>
      <description>&lt;p&gt;Really wanted to build a project from scratch... so I did. &lt;/p&gt;

&lt;p&gt;Project turned into a command line utility for daily journaling - &lt;a href="https://github.com/skatkov/stoic"&gt;stoic&lt;/a&gt;. I was mainly motivated by my desire to practice golang in a greenfield project and with an itch to scratch my own niche (meaning, finally writing software that I would use myself). &lt;/p&gt;

&lt;p&gt;At some point, after writing in various programming languages you start to wonder... Will another language make that big of a difference to justify time spent on mastering it? &lt;/p&gt;

&lt;p&gt;In case of golang learning curve was really low. It might be language, it might be my approach to learning new language or might be combination of two. Here are some of the things, that made it easy for me to learn it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;p&gt;One of the big reasons I got interested in golang, was huge amount of awesome go projects in open source. I personally use roughly around ~10 golang based project on a computer I'm sitting at now.&lt;/p&gt;

&lt;p&gt;I couldn't stop reading source code - it was easy to digest, even without prior experience in language! So, there have become a moment where I said "this seems easy, I should try writing my app with go". Of course, a lot of that available open source not only inspired, but show me examples for possible solutions that I was looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Github Copilot
&lt;/h2&gt;

&lt;p&gt;Copilot is amazingly good. In some cases it fills in 80% of a method I'm planning to write and almost always guesses assertions in tests that I about to write.&lt;/p&gt;

&lt;p&gt;With Copilot I didn't need to disrupt my coding flow. Wrote a descriptive method name and AI filled in the "blanks", I tweaked the hell out of it, especially with auto-complete - it's easy.&lt;/p&gt;

&lt;p&gt;It's creepy, how fast Copilot allowed me to pick up a language.&lt;/p&gt;

&lt;h2&gt;
  
  
  BubbleTea
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/charmbracelet/bubbletea"&gt;BubbleTea TUI&lt;/a&gt; framework and charm related libraries is amazingly great looking. It also follows ELM architecture and this architecture works great with golang. I was blown away to find a framework of such a quality and with so much simplicity!&lt;/p&gt;

&lt;p&gt;I need to write more cli tools! This BubbleTea TUI thing is just incredible, a bit hard to test though.&lt;/p&gt;

&lt;h1&gt;
  
  
  Documentation
&lt;/h1&gt;

&lt;p&gt;I generally try to code without google/stackoverflow these days, mostly relying on official API documentation. I always stumble on something interesting there, something that is usually omitted in a stackoverflow answer! &lt;/p&gt;

&lt;p&gt;In this regard, golang documentation was mostly great!&lt;/p&gt;

&lt;p&gt;So to sum it up, I must say, even against my initial prejudgement (not a functional language, duhhh) - I really enjoyed diving into Go and absolutely will continue to explore this language further. I already have couple of projects I'd want to build with it too.&lt;/p&gt;

&lt;p&gt;Golang turned out to be a valuable asset to have -- only the fact, that it can easily compile for any platform! Damn it's nice! I'm eagerly looking for new projects to build with it now.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Lessons from building another URL shortener</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Thu, 26 Mar 2020 17:55:08 +0000</pubDate>
      <link>https://dev.to/skatkov/lessons-from-building-another-url-shortener-1gpo</link>
      <guid>https://dev.to/skatkov/lessons-from-building-another-url-shortener-1gpo</guid>
      <description>&lt;p&gt;&lt;strong&gt;This post was originally published &lt;a href="https://skatkov.com/2020/03/26/lessons-from-building-another-url-shortener.html"&gt;on my website&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm a software consultant working mostly with the web.&lt;/p&gt;

&lt;p&gt;I secretly wanted to build an URL shortener for quite some time.  It's one of those projects that seems relatively easy at first, but during my work as a consultant, I stumbled upon in-house solutions that caused more harm in the long term, than it was worth.&lt;/p&gt;

&lt;p&gt;I’ve always wondered what my solution would be to the problems I came across and what would be the best solutions to overcome them. I recently managed to find the time and right idea to build one. In this post, I'd like to highlight my key takeaways and design principles that I put into the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing "the project"
&lt;/h2&gt;

&lt;p&gt;There is not much to gain from a toy-project, it will end up sitting on a hard drive collecting dust. So, how can one fully experience all the consequences of the technical choices with a toy project?&lt;/p&gt;

&lt;p&gt;I came up with a project that can potentially generate profit or at least could be sustainable to operate.  There is no shortage of URL shorteners out there, therefore I decided to build a &lt;a href="https://acart.to"&gt;very niche one for Amazon Affiliates called aCart.to&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;It works like a regular shortener, but as input it takes a list of Product IDs instead of URL. Based on the list, the system pre-builds and stores URL that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redirects to the amazon cart page with products pre-added&lt;/li&gt;
&lt;li&gt;could link to multiple products&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After careful consideration, I’ve highlighted the 3 most important things to consider in a project like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a response code wisely
&lt;/h2&gt;

&lt;p&gt;If the browser requests a URL from an external server, it would receive a &lt;a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes"&gt;response code&lt;/a&gt; with a requested page. When using a URL shortener, page content will not be returned, but a redirect request will be issued with the following &lt;em&gt;response code&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If a shortener will be used by others, it's crucial to have a &lt;strong&gt;301 response code&lt;/strong&gt;. If you’re building a shortener for internal use - a &lt;strong&gt;302 response code&lt;/strong&gt; could be more appropriate. For any technically savvy user this would be the biggest criteria for a choice. So it's important to understand the difference to make the right call.&lt;/p&gt;

&lt;h3&gt;
  
  
  301 redirect
&lt;/h3&gt;

&lt;p&gt;A 301 redirect says that the URL requested (&lt;em&gt;short URL&lt;/em&gt;) has “permanently” moved to the &lt;em&gt;long URL&lt;/em&gt;. Since it’s a permanent redirect, search engines finding links to the short URLs will credit all those links to the &lt;em&gt;long URL&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  302 redirect
&lt;/h3&gt;

&lt;p&gt;302 redirect is a “temporary” one. If that’s issued, search engines assume that the &lt;em&gt;short URL&lt;/em&gt; is the “real” URL and just temporarily being pointed elsewhere. That means link credit does not get passed on to the &lt;em&gt;long URL&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other
&lt;/h3&gt;

&lt;p&gt;Some URL shorteners on a market are very creative and manage to use 200 or 303 codes while redirecting requests. I’m not completely aware of the motivations for doing so, but it seems “shady” and I would personally recommend against such a practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it (really) short!
&lt;/h2&gt;

&lt;p&gt;Since the biggest use-case for shorteners are social networks with limited character capacity per message, then it's really important to lose all the fat from a short URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aim for a short domain
&lt;/h3&gt;

&lt;p&gt;The first step in the right direction should be your domain name - it should be as short as possible. Keep in mind that finding short  &lt;code&gt;.com&lt;/code&gt; or &lt;code&gt;.org&lt;/code&gt; domains will be costly, so you might want to look for &lt;a href="https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains#Country_code_top-level_domains"&gt;tld domains of other countries&lt;/a&gt;. (&lt;code&gt;.ai&lt;/code&gt; sound really cool!) These domains come with a SEO penalty and you’d be less likely to rank in google, but they are short. That’s the tradeoff most shorteners go with.&lt;/p&gt;

&lt;p&gt;But there is even more fat we can lose here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lose www, but don't lose performance and security
&lt;/h3&gt;

&lt;p&gt;A lot of people argue that "&lt;a href="http://www.domain.com"&gt;www.domain.com&lt;/a&gt;" vs "domain.com" usage in domains is merely a cosmetic difference. But unfortunately, dropping &lt;code&gt;www&lt;/code&gt; from URL can have dire consequences because of how DNS records work.&lt;/p&gt;

&lt;p&gt;Unfortunately, most DNS providers work only with two domain record types - &lt;em&gt;A record&lt;/em&gt; or &lt;em&gt;CNAME record&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;CNAME record&lt;/em&gt; - requires that it be the only record for that domain&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;A record&lt;/em&gt; - can only point to a IP address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In most cases, to use domains with no ‘www’, you’re required to use A-record with a pointer to the DNS provider's load balancer. In case of DDoS attack on the DNS provider, your domain will be very likely to stop answering.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A record&lt;/em&gt; also comes with a performance hit, requests for &lt;em&gt;CNAME record&lt;/em&gt; could be served by a closest server. Requests for &lt;em&gt;A record&lt;/em&gt; always get resolved through one instance, it will probably be the most loaded instance available.&lt;/p&gt;

&lt;p&gt;Netlify goes into great lengths to explain this problem in a blog post titled &lt;a href="https://www.netlify.com/blog/2017/02/28/to-www-or-not-www/"&gt;“To www or not www”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My solution was to move the domain over to a DNS provider that could handle &lt;strong&gt;ALIAS records&lt;/strong&gt; or &lt;strong&gt;CNAME Flattering&lt;/strong&gt;. In my case, I had to move from &lt;a href="https://namecheap.com"&gt;namecheap&lt;/a&gt; to &lt;a href="https://dnsimple.com"&gt;dnsimple&lt;/a&gt;. Support for this feature usually costs an additional monthly fee. &lt;/p&gt;

&lt;h3&gt;
  
  
  Bijective function for “hashing”
&lt;/h3&gt;

&lt;p&gt;The shortener has to come up with a public ID that uniquely identifies shortened URLs. In a lot of cases, clients will rule against your server if you're stably generating a long hash for a public ID.&lt;/p&gt;

&lt;p&gt;I used a bijective function that could transform a number into a hashed text and inverse the text back into a number. Thanks to the internet, it wasn’t that hard to find an &lt;a href="https://stackoverflow.com/questions/742013/how-do-i-create-a-url-shortener/742047#742047"&gt;example on stackoverflow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This function significantly simplified table design in the database. I’m able to convert the primary key to a hash text and pinpoint exact records in a table based on hash text. Not having other indexes in a table is a great speed improvement on it’s own. &lt;/p&gt;

&lt;h2&gt;
  
  
  Don't lose referrals
&lt;/h2&gt;

&lt;p&gt;HTTP Referrer is an option in the HTTP header field that stores the address of the webpage that this request originated from.&lt;/p&gt;

&lt;p&gt;There is a very thin line between a URL shortener service and a referrer cloaking service (like &lt;a href="http://www.nullrefer.com"&gt;nullrefer&lt;/a&gt;). And this thin line is called "Pass HTTP Referrer". You probably don't want to unintentionally become a cloaking service.&lt;/p&gt;

&lt;p&gt;Passing referrer information along to a long URL in ruby is just one line of code, but a really important line of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_REFERER"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;referrer&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;referrer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, there are a number of cases when the HTTP_REFERER parameter will be empty or could not be passed. Due to specification, HTTP_REFERER will not be passed if a long URL uses insecure protocol (http://). &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a website is accessed from a HTTP Secure (HTTPS) connection and a link points to anywhere except another secure location, then the referer field is not sent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But nowadays we have &lt;a href="https://w3c.github.io/webappsec-referrer-policy/"&gt;Referrer Policy&lt;/a&gt; as a solution that could partially mitigate these issues by just adding meta tag to HTML pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;meta name="referrer" content="origin"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;So, when it comes to launching a URL shortener service, keep in mind that these three issues are the biggest you need to be aware of. A lot of other articles explain how to write this service, but don’t give you a project wide overview of launching it into the public.&lt;/p&gt;

&lt;p&gt;I hope you found this article useful. If you need a hand in thinking through some of your existing projects - I’m also available for hire!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>New deadline for Product Advertising API deprecation</title>
      <dc:creator>Stanislav(Stas) Katkov</dc:creator>
      <pubDate>Fri, 08 Nov 2019 09:18:19 +0000</pubDate>
      <link>https://dev.to/skatkov/new-deadline-for-product-advertising-api-deprecation-2p94</link>
      <guid>https://dev.to/skatkov/new-deadline-for-product-advertising-api-deprecation-2p94</guid>
      <description>&lt;p&gt;&lt;a href="https://fbacatalog.com/blog/new-product-adveritising-api.html"&gt;As I previously announced&lt;/a&gt;, Amazon was planning to deprecate PA-API version 4 on 31 October 2019. But that didn't happen and the deadline has been pushed back.&lt;/p&gt;

&lt;p&gt;Amazon keeps tradition of secrecy on this API transition. To this date not a single company representative officially commented in support forums and we have seen zero public announcements, besides those details provided in &lt;a href="https://webservices.amazon.com/paapi5/documentation/"&gt;documentation for new API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In a private communication, some of the biggest Amazon partners that rely on PA-API have received a heads up that deprecation will be postponed. But they made them sign NDA, justifying this by the push to get everyone off older API as fast as possible.&lt;/p&gt;

&lt;p&gt;PA-API has a huge history of abuse by people that scrape Amazon for data. And last year changes have been mostly aimed to tame all the abusive behavior. This migration to new API is not an exception -- biggest features have been improved security and even further limits to grab big chunks of data at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8o2c_TMt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://fbacatalog.com/images/munich-event.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8o2c_TMt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://fbacatalog.com/images/munich-event.jpeg" alt="leaked photo from Amazon Associate event in Munich"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We've found leaked photo from Amazon Associate event in Munich (hence the German language) that suggest that &lt;strong&gt;deprecation has been pushed back to 14 January&lt;/strong&gt;. There seems to be a typo in the year, but our private sources have confirmed this date.&lt;/p&gt;

&lt;p&gt;This push back, makes a lot of sense -- it would not be reasonable to do such huge change during biggest sales season. So let's breathe calmly here, we have some time to migrate our websites and apps.&lt;/p&gt;

&lt;p&gt;Thanks to Amazon for that!&lt;/p&gt;

</description>
      <category>productadvertisingapi</category>
      <category>amazon</category>
      <category>api</category>
    </item>
  </channel>
</rss>
