Pin CDN dependencies and add SRI hashes
Alpine.js 3.15.8, Alpine Persist 3.15.0, OverType 2.1.1.

Assisted-by: Claude Opus 4.6 via pi
change rrmzxlqwlquyzskmymqlrorwmsomonvr
commit d9c4c3fba881b6cf6e3e09e15903b8a2ba2517e4
author Alpha Chen <alpha@kejadlen.dev>
date
parent tntrmkvv
diff --git a/AGENTS.md b/AGENTS.md
index b8be0a4..00f8f13 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -98,5 +98,5 @@ Output goes to `~/.cache/ketchup/snapshots/` (or `$XDG_CACHE_HOME`). Templates f
 - **Migrations:** Sequel migrations in `db/migrate/`, numbered sequentially (`001_`, `002_`, …). Migrations auto-run on boot.
 - **User identification:** Current user from `HTTP_TAILSCALE_USER_LOGIN` / `HTTP_TAILSCALE_USER_NAME` request headers.
 - **Testing:** Minitest with `Rack::Test`. Fake Tailscale headers via helper.
-- **Client-side:** Alpine.js for reactivity, Alpine Persist for state persistence, OverType for markdown editing. No build step — all loaded via CDN.
+- **Client-side:** Alpine.js for reactivity, Alpine Persist for state persistence, OverType for markdown editing. No build step — all loaded via CDN with pinned versions and SRI hashes in `views/layout.rb`. To update a dependency: fetch the new versioned URL, generate a hash with `curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A`, and update both the `src` and `integrity` attributes.
 - **Ownership scoping:** User has `many_through_many :tasks` through `:series`. Routes use `@user.tasks_dataset` and `@user.series_dataset` to scope lookups.
diff --git a/lib/ketchup/views/layout.rb b/lib/ketchup/views/layout.rb
index 331a386..9265682 100644
--- a/lib/ketchup/views/layout.rb
+++ b/lib/ketchup/views/layout.rb
@@ -19,10 +19,16 @@ module Views
           link(rel: "stylesheet", href: "/css/reset.css")
           link(rel: "stylesheet", href: "/css/utopia.css")
           link(rel: "stylesheet", href: "/css/app.css")
-          script(src: "https://unpkg.com/overtype/dist/overtype.min.js")
+          script(src: "https://unpkg.com/overtype@2.1.1/dist/overtype.min.js",
+                 integrity: "sha384-zp8RL0j4VLfaFKgqehca9l8rfcE4Jh0Nt1CFoVyUBn+qa4velUokXJXsW2h0J5xT",
+                 crossorigin: "anonymous")
           script(src: "/js/app.js", defer: true)
-          script(src: "https://cdn.jsdelivr.net/npm/@alpinejs/persist@3/dist/cdn.min.js", defer: true)
-          script(src: "https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js", defer: true)
+          script(src: "https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.15.0/dist/cdn.min.js",
+                 integrity: "sha384-6WOLkykwLb3YWzXZ6lAq+GI0p3V+enUm9jY6yIXGpIriiAUOSF5dgNJLoSSNam4j",
+                 crossorigin: "anonymous", defer: true)
+          script(src: "https://cdn.jsdelivr.net/npm/alpinejs@3.15.8/dist/cdn.min.js",
+                 integrity: "sha384-LXWjKwDZz29o7TduNe+r/UxaolHh5FsSvy2W7bDHSZ8jJeGgDeuNnsDNHoxpSgDi",
+                 crossorigin: "anonymous", defer: true)
         end
         body do
           header(class: "site-header") do