1<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png"/><link rel="stylesheet" href="/_next/static/css/0d16c72050851854.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/7ffcc02a3c67389b.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/c20e76e7d11dec32.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/59fefb2212bd5b60.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-55b36f0cd6c208fe.js"/>
1<script src="/_next/static/chunks/4bd1b696-100b9d70ed4e49c1.js" async=""></script>
1<script src="/_next/static/chunks/9da6db1e-8a4e8c23641c31bf.js" async=""></script>
1<script src="/_next/static/chunks/1255-a242e0ce0372fddf.js" async=""></script>
1<script src="/_next/static/chunks/main-app-bda8a52fe841b3aa.js" async=""></script>
1<script src="/_next/static/chunks/4554-984b480a9db5f3fe.js" async=""></script>
1<script src="/_next/static/chunks/7563-4dfa0994a9b0e37c.js" async=""></script>
1<script src="/_next/static/chunks/9216-9272709ba40c1cfd.js" async=""></script>
1<script src="/_next/static/chunks/2810-6b74d77b4b0dc790.js" async=""></script>
1<script src="/_next/static/chunks/3091-bf1537ff6571a1bd.js" async=""></script>
1<script src="/_next/static/chunks/app/layout-6ae7b4b583d4bba3.js" async=""></script>
1<script src="/_next/static/chunks/app/not-found-b861935655c5698b.js" async=""></script>
1<script src="/_next/static/chunks/5001-fae0bebdd93f95e3.js" async=""></script>
1<script src="/_next/static/chunks/2423-c8b1388240338d7d.js" async=""></script>
1<script src="/_next/static/chunks/652-f40dcb480b4e356f.js" async=""></script>
1<script src="/_next/static/chunks/730-8191af9e9424b148.js" async=""></script>
1<script src="/_next/static/chunks/app/page-cc18ae1a3a7a5ed8.js" async=""></script>
1<script src="/_next/static/chunks/0dbeb660-9e82e99813552576.js" async=""></script>
1<script src="/_next/static/chunks/1545-7abf96433ff6553f.js" async=""></script>
1<script src="/_next/static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js" async=""></script>
1<meta name="next-size-adjust" content=""/><title>Elevate Your Flutter Web Hosting with Dart_Frog</title><meta name="description" content="Link to the GitHub Repo 2Everyone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ..."/><link rel="canonical" href="https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog"/><meta property="og:title" content="Elevate Your Flutter Web Hosting with Dart_Frog"/><meta property="og:description" content="Link to the GitHub Repo 3Everyone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ..."/><meta property="og:url" content="https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog"/><meta property="og:site_name" content="jxstxn"/><meta property="og:image" content="https://cdn.hashnode.com/res/hashnode/image/upload/v1691744613789/9a5f451a-bf52-459e-8b99-c91e5e592469.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:type" content="article"/><meta property="article:published_time" content="2023-08-11T09:05:26.899Z"/><meta property="article:author" content="https://jxstxn.dev/@jxstxn1"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:creator" content="https://twitter.com/jxstxn_____"/><meta name="twitter:title" content="Elevate Your Flutter Web Hosting with Dart_Frog"/><meta name="twitter:description" content="Link to the GitHub Repo 4Everyone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ..."/><meta name="twitter:image" content="https://cdn.hashnode.com/res/hashnode/image/upload/v1691744613789/9a5f451a-bf52-459e-8b99-c91e5e592469.png"/><link rel="icon" href="/favicon.ico"/>
4<script type="text/javascript">window.addEventListener('message',function(e){var d=e.data;if(!d||d.__hnWidget!==1||typeof d.h!=='number'||d.h<=0)return;var f=document.querySelectorAll('.hn-widget-frame');for(var i=0;i<f.length;i++){if(f[i].contentWindow===e.source){f[i].style.height=d.h+'px';break;}}});</script>
4<script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script>
4</head><body class="__variable_c2612a __variable_246ccd __variable_3069d5 bg-background dark:bg-background"><div hidden=""><!--$--><!--/$--></div>
4<script>((a2, b2, c4, d3, e2, f2, g4, h3) => { 5 let i3 = document.documentElement, j2 = ["light", "dark"]; 6 function k2(b3) { 7 var c5; 8 (Array.isArray(a2) ? a2 : [a2]).forEach((a3) => { 9 let c6 = a3 === "class", d4 = c6 && f2 ? e2.map((a4) => f2[a4] || a4) : e2; 10 c6 ? (i3.classList.remove(...d4), i3.classList.add(f2 && f2[b3] ? f2[b3] : b3)) : i3.setAttribute(a3, b3); 11 }), c5 = b3, h3 && j2.includes(c5) && (i3.style.colorScheme = c5); 12 } 13 if (d3) k2(d3); 14 else try { 15 let a3 = localStorage.getItem(b2) || c4, d4 = g4 && a3 === "system" ? window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : a3; 16 k2(d4); 17 } catch { 18 } 19 })("class","theme","system",null,["light","dark"],null,true,true)</script>
19<main id="main-content" class="min-h-screen bg-transparent"><header class="sticky top-0 z-50 w-full py-4 bg-background backdrop-blur-sm transition-transform duration-300 translate-y-0"><a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-background focus:px-4 focus:py-2 focus:text-sm focus:shadow">Skip to main content</a><div class="relative w-full mx-auto xl:max-w-[80rem] 2xl:max-w-[90rem] px-4 sm:px-12 md:px-18 lg:px-16 xl:px-10"><div class="flex items-center justify-between gap-3 xl:hidden"><div class="flex items-center gap-3 min-w-0 flex-1"><a href="https://hashnode.com/?utm_source=https%3A%2F%2Fjxstxn.dev&utm_medium=referral&utm_campaign=blog_header_logo&utm_content=logo" class="inline-flex items-center justify-center text-lg text-muted-foreground hover:text-foreground transition border rounded-md h-9 w-9 shrink-0 border-border hover:bg-muted"><i class="text-xl fa-brands fa-hashnode" aria-hidden="true"></i><span class="sr-only">Hashnode</span></a><a title="jxstxn" class="text-lg font-semibold text-foreground truncate min-w-0" href="/">jxstxn</a></div><div class="flex items-center gap-2 shrink-0"><button class="gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent py-2 h-9 w-9 px-0 flex items-center justify-center text-muted-foreground hover:text-foreground bg-transparent border-border shadow-none" aria-label="Open search (press Control or Command and K)"><i class="fa-jelly fa-search text-sm" aria-hidden="true"></i><span class="sr-only">Open search (press Control or Command and K)</span></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent w-9 px-3 h-9 text-muted-foreground hover:text-foreground shadow-none" type="button" id="radix-_R_2j4anpfalb_" aria-haspopup="menu" aria-expanded="false" data-state="closed" data-slot="dropdown-menu-trigger"><i class="fa-jelly fa-sun text-sm scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" aria-hidden="true"></i><i class="fa-jelly fa-moon absolute text-sm scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" aria-hidden="true"></i><span class="sr-only">Toggle theme</span></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground py-2 h-9 w-10 px-0 shadow-none" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="radix-_R_3j4anpfalb_" data-state="closed" data-slot="sheet-trigger"><i class="fa-jelly fa-bars" aria-hidden="true"></i><span class="sr-only">Open menu</span></button></div></div><div class="hidden xl:grid gap-4 items-center xl:grid-cols-[1fr_auto_1fr] xl:gap-6 max-w-full xl:mx-auto xl:max-w-[80rem] 2xl:max-w-[90rem]"><div class="flex items-center w-full gap-3 min-w-0"><a href="https://hashnode.com/?utm_source=https%3A%2F%2Fjxstxn.dev&utm_medium=referral&utm_campaign=blog_header_logo&utm_content=logo" class="inline-flex items-center justify-center text-lg text-muted-foreground hover:text-foreground transition border rounded-md h-9 w-9 border-border hover:bg-muted"><i class="text-xl fa-brands fa-hashnode" aria-hidden="true"></i><span class="sr-only">Hashnode</span></a><span class="h-6 border-l border-border"></span><p class="font-semibold text-foreground min-w-0 truncate text-lg sm:text-xl"><a title="jxstxn" href="/">jxstxn</a></p></div><div></div><div class="flex items-center justify-end gap-3 text-muted-foreground"><button class="gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focu
19s-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent py-2 h-9 w-9 px-0 flex items-center justify-center text-muted-foreground hover:text-foreground bg-transparent border-border shadow-none" aria-label="Open search (press Control or Command and K)"><i class="fa-jelly fa-search text-sm" aria-hidden="true"></i><span class="sr-only">Open search (press Control or Command and K)</span></button><div class="flex items-center gap-3"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent w-9 px-3 h-9 text-muted-foreground hover:text-foreground shadow-none" type="button" id="radix-_R_6t4anpfalb_" aria-haspopup="menu" aria-expanded="false" data-state="closed" data-slot="dropdown-menu-trigger"><i class="fa-jelly fa-sun text-sm scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" aria-hidden="true"></i><i class="fa-jelly fa-moon absolute text-sm scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" aria-hidden="true"></i><span class="sr-only">Toggle theme</span></button><a target="_blank" rel="noopener" class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground py-2 gap-2 px-3 h-9 shadow-none" href="https://hashnode.com"><i class="text-sm fa-jelly fa-pencil" aria-hidden="true"></i>Write</a></div></div></div></div></header><div data-slot="dialog-header" class="flex flex-col gap-2 text-center sm:text-left sr-only"><h2 id="radix-_R_8anpfalbH1_" data-slot="dialog-title" class="text-lg leading-none font-semibold">Command Palette</h2><p id="radix-_R_8anpfalbH2_" data-slot="dialog-description" class="text-muted-foreground text-sm">Search for a command to run...</p></div><section class="py-8"><div class="px-4 sm:px-8"><div class="w-full max-w-full xl:mx-auto xl:max-w-[80rem] 2xl:max-w-[90rem]"><div class="grid gap-5 max-w-full"><article class="w-full max-w-full rounded-xl border bg-card text-card-foreground"><div class="px-5 pt-12 pb-8 sm:px-8"><header class="space-y-5"><div class="space-y-4 text-center max-w-4xl mx-auto"><h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold leading-tight tracking-tight text-balance text-foreground">Elevate Your Flutter Web Hosting with Dart_Frog</h1><p class="text-lg sm:text-xl leading-normal font-normal text-muted-foreground text-balance">(and some more packages)</p><div class="flex flex-wrap items-center justify-center gap-3 text-sm text-muted-foreground"><div class="flex items-center gap-2"><span>Published</span><time dateTime="2023-08-11T09:05:26.899Z">August 11, 2023</time></div><span class="text-muted-foreground">â¢</span><span>6<!-- --> min read</span><span class="text-muted-foreground">â¢</span><a href="/elevate-your-flutter-web-hosting-with-dartfrog.md" target="_blank" rel="noopener" class="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"><i class="fa-brands fa-markdown" aria-hidden="true"></i>View as Markdown</a></div></div></header></div><div class="w-full"><img alt="Elevate Your Flutter Web Hosting with Dart_Frog" width="1200" height="750" decoding="async" data-nimg="1" class="w-full h-auto" style="color:transparent" src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png"/></div><div class="px-5 py-12 sm:px-8 space-y-8"><div class="max-w-2xl mx-auto"><div class="flex flex-wrap items-center gap-5"><div class="flex items-center gap-5"><div class="flex gap-3 items-start"><a target="_blank" rel="noopener" href="https://hashnode.com/@jxstxn1"><span class="relative flex shrink-0 overflow-hidden rounded-full h-10 w-10 border border-border transition-opacity hover:opacity-80"><span class="flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground font-semibold text-lg">J</span></span></a><div class="min-w-0"><div class="flex items-center gap-2"><a target="_blank" rel="noopener" class="font-medium text-foreground hover:text-primary transition-colors" href="https://hashnode.com/@jxstxn1">Justin Baumann</a><div class="flex items-center gap-1.5 text-sm"><a target="_blank" rel="noopener" class="text-muted-foreground hover:text-foreground transition-colors" aria-label="X (Twitter)" href="https://twitter.com/jxstxn_____"><i class="fa-brands fa-x-twitter" aria-hidden="true"></i></a><a target="_blank" rel="noopener" class="text-muted-foreground hover:text-foreground transition-colors" aria-label="LinkedIn" href="https://www.linkedin.com/in/justin-baumann-flutter/"><i class="fa-brands fa-linkedin" aria-hidden="true"></i></a></div></div><div class="text-sm text-muted-foreground line-clamp-2"><p>Hey, I'm jxstxn or Justin. I'm a Flutter Developer who loves Flutter and Dart. In my free time, I'm writing Dart Packages like Middlewares for Shelf or Plugins for Sidekick.</p> 20</div></div></div></div></div></div><div class="relative"><aside class="hidden xl:block absolute top-0 left-0 h-full"><nav class="sticky top-22 w-44"><div class="hidden xl:block group/toc relative"><div class="grid"><div class="[grid-area:1/1] opacity-0 group-hover/toc:opacity-100 transition-opacity duration-200 max-h-[calc(100vh-3rem)] overflow-y-auto"><p class="text-sm font-semibold uppercase tracking-wide text-muted-foreground mb-3">On this page</p><nav aria-label="On this page"><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-key-topics">Key Topics</a><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-setting-everything-up">Setting everything up</a><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-build-and-copy-the-flutter-app">Build and copy the Flutter App</a><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-serve-flutter-app-at">Serve Flutter App at /</a><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-creating-our-middleware">Creating our middleware</a><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-create-hashes-for-inline-scripts-with-the-csphasher-package">Create hashes for inline-scripts with the csp_hasher package</a><a class="block py-1 text-sm transition-colors hover:text-foreground text-muted-foreground" style="padding-left:0.75rem" href="#heading-conclusion">Conclusion</a></nav></div><div class="[grid-area:1/1] relative flex flex-col gap-px transition-opacity duration-200 group-hover/toc:opacity-0 pointer-events-none self-start max-h-[calc(100vh-3rem)] overflow-hidden pl-3"><div class="absolute left-0 w-0.5 rounded-full bg-foreground transition-all duration-300 opacity-0" style="transform:translateY(0px);height:12px"></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:10px"></div></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:21px"></div></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:30px"></div></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:22px"></div></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:23px"></div></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:48px"></div></div><div class="flex items-center h-3" style="padding-left:10px"><div class="h-0.5 rounded-full transition-all duration-300 bg-border" style="width:10px"></div></div></div></div></div></nav></aside><div class="max-w-2xl mx-auto min-w-0 contain-[inline-size]"><div class="prose dark:prose-invert prose-pre:bg-foreground prose-pre:text-background prose-pre:border prose-pre:border-border prose-pre:rounded prose-pre:p
20x-5 prose-pre:py-4 dark:prose-pre:bg-muted dark:prose-pre:text-foreground dark:prose-pre:border-border prose-pre:overflow-x-auto max-w-none text-base sm:text-lg [&>div>p:first-child]:mt-0 [&>div>p:first-child]:pt-0 min-w-0 wrap-break-word [&_a]:break-all **:max-w-full"><div><p><a target="_blank" href="https://github.com/jxstxn1/hash_demo" rel="noopener ugc">Link to the GitHub Repo</a></p> 21<p>Everyone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the following:</p> 22<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691525897533/5137f6e7-1424-4949-bdf9-352276200e3e.png" alt class="image--center mx-auto" /></p> 23<p>This can be very frustrating especially if you start whitelisting scripts to your Content Security Policy and those scripts are requesting resources from somewhere else.</p> 24<p><strong>So what's the solution for this?</strong></p> 25<p>Content Security Policy errors can only be solved by the web server which is hosting the Web App. There are probably multiple solutions to tackle this problem, but today we are going to take a deep dive into <code>dart_frog</code> and how to utilize it to host our Flutter Web App with some advanced capabilities.</p> 26<h3 id="heading-key-topics">Key Topics</h3> 27<blockquote> 28<p>Note: Content Security Policy will be called CSP in this Blog Post</p> 29</blockquote> 30<ul> 31<li><p>Serve a Flutter Web App with <code>dart_frog</code></p> 32</li> 33<li><p>Set all recommended security headers with <code>shelf_helmet</code></p> 34</li> 35<li><p>Create a Content Security Policy that works with CSP2 and 3</p> 36</li> 37<li><p>Create Hashes for <code>inline-scripts</code> to whitelist them in our Content Security Policy</p> 38</li> 39</ul> 40<h3 id="heading-setting-everything-up">Setting everything up</h3> 41<p>We'll start by creating a brand new dart_frog project</p> 42<pre><code class="lang-bash">dart_frog create hash_demo 43</code></pre> 44<p>We can then open our <code>pubpsec.yaml</code> and replace the contents with</p> 45<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">hash_demo</span> 46<span class="hljs-attr">description:</span> <span class="hljs-string">An</span> <span class="hljs-string">example</span> <span class="hljs-string">of</span> <span class="hljs-string">how</span> <span class="hljs-string">to</span> <span class="hljs-string">use</span> <span class="hljs-string">the</span> <span class="hljs-string">CSP</span> <span class="hljs-string">Hasher</span> <span class="hljs-string">package</span> <span class="hljs-string">in</span> <span class="hljs-string">combination</span> <span class="hljs-string">with</span> <span class="hljs-string">shelf_helmet.</span> 47<span class="hljs-attr">version:</span> <span class="hljs-number">1.0</span><span class="hljs-number">.0</span><span class="hljs-string">+1</span> 48<span class="hljs-attr">publish_to:</span> <span class="hljs-string">none</span> 49 50<span class="hljs-attr">environment:</span> 51 <span class="hljs-attr">sdk:</span> <span class="hljs-string">">=3.0.0 <4.0.0"</span> 52 53<span class="hljs-attr">dependencies:</span> 54 <span class="hljs-attr">csp_hasher:</span> <span class="hljs-string">^1.0.0</span> 55 <span class="hljs-attr">dart_frog:</span> <span class="hljs-string">^1.0.0</span> 56 <span class="hljs-attr">path:</span> <span class="hljs-string">^1.8.3</span> 57 <span class="hljs-attr">shelf_helmet:</span> <span class="hljs-string">^2.1.1</span> 58 59<span class="hljs-attr">dev_dependencies:</span> 60 <span class="hljs-attr">mocktail:</span> <span class="hljs-string">^0.3.0</span> 61 <span class="hljs-attr">test:</span> <span class="hljs-string">^1.19.2</span> 62 <span class="hljs-attr">very_good_analysis:</span> <span class="hljs-string">^5.0.0</span> 63</code></pre> 64<p>Now we have to create our Flutter project with:<br /><code>flutter create counter --platform web</code></p> 65<h3 id="heading-build-and-copy-the-flutter-app">Build and copy the Flutter App</h3> 66<p>To build our Flutter App we run in <code>counter</code>:</p> 67<pre><code class="lang-bash">flutter build web --web-renderer canvaskit --release --csp 68</code></pre> 69<p>After the build, we can copy the output from <code>counter/build/web</code> to <code>public/</code><br />To simplify this you can run from <code>hash_demo</code>:</p> 70<pre><code class="lang-bash">cp -r counter/build/web/ public/ 71</code></pre> 72<h3 id="heading-serve-flutter-app-at">Serve Flutter App at /</h3> 73<p>To serve the <code>index.html</code> file directly at the <code>/</code> location we modify the <code>routes/index.dart</code> file with:</p> 74<pre><code class="lang-dart"><span class="hljs-keyword">
74import</span> <span class="hljs-string">'dart:io'</span>; 75 76<span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 77<span class="hljs-keyword">import</span> <span class="hljs-string">'package:path/path.dart'</span> <span class="hljs-keyword">as</span> path; 78 79Response onRequest(RequestContext context) { 80 <span class="hljs-keyword">final</span> file = File( 81 path.join(Directory.current.path, <span class="hljs-string">'public'</span>, <span class="hljs-string">'index.html'</span>), 82 ); 83 <span class="hljs-keyword">final</span> indexHtml = file.readAsStringSync(); 84 <span class="hljs-keyword">return</span> Response(body: indexHtml, headers: {<span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'text/html'</span>}); 85} 86</code></pre> 87<p>To ensure that everything works as expected we will write a test for it.<br />Let's create <code>test/routes/index_test.dart</code> :</p> 88<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>; 89 90<span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 91<span class="hljs-keyword">import</span> <span class="hljs-string">'package:mocktail/mocktail.dart'</span>; 92<span class="hljs-keyword">import</span> <span class="hljs-string">'package:test/test.dart'</span>; 93<span class="hljs-keyword">import</span> <span class="hljs-string">'../../routes/index.dart'</span> <span class="hljs-keyword">as</span> route; 94 95<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MockRequestContext</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Mock</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">RequestContext</span> </span>{} 96 97<span class="hljs-keyword">final</span> htmlString = File( 98 <span class="hljs-string">'<span class="hljs-subst">${Directory.current.path}</span>/public/index.html'</span>, 99).readAsStringSync(); 100 101<span class="hljs-keyword">void</span> main() { 102 group(<span class="hljs-string">'GET /'</span>, () { 103 test(<span class="hljs-string">'responds with a 200, an html and the content_type header text/html'</span>, 104 () <span class="hljs-keyword">async</span> { 105 <span class="hljs-keyword">final</span> context = _MockRequestContext();
106 <span class="hljs-keyword">final</span> response = route.onRequest(context); 107 expect(response.statusCode, equals(<span class="hljs-number">200</span>)); 108 expect( 109 response.headers, 110 equals({ 111 <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'text/html'</span>, 112 <span class="hljs-string">'content-length'</span>: <span class="hljs-string">'1830'</span>, 113 }), 114 ); 115 expect(response.body(), completion(htmlString)); 116 }); 117 }); 118} 119</code></pre> 120<p>Perfect! We can now serve our Flutter Web App at <code>localhost:8080/</code><br />To test it in a browser you can run:</p> 121<pre><code class="lang-bash">dart_frog dev 122</code></pre> 123<h3 id="heading-creating-our-middleware">Creating our middleware</h3> 124<p>We can now create our middleware where most of the magic is happening.<br />To do so run:</p> 125<pre><code class="lang-bash">dart_frog new middleware / 126</code></pre> 127<p>We can now replace the content in the newly created middleware with this:</p> 128<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 129<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shelf_helmet/shelf_helmet.dart'</span>; 130 131Handler middleware(Handler handler) { 132 <span class="hljs-keyword">return</span> handler.use(requestLogger()).use( 133 fromShelfMiddleware( 134 helmet( 135 options: <span class="hljs-keyword">const</span> HelmetOptions( 136 cspOptions: ContentSecurityPolicyOptions.useDefaults( 137 directives: { 138 <span class="hljs-string">'script-src'</span>: [ 139 <span class="hljs-string">"'strict-dynamic'"</span>, 140 <span class="hljs-string">"'wasm-unsafe-eval'"</span>, 141 <span class="hljs-string">"'self'"</span>, 142 <span class="hljs-string">'blob:'</span>, 143 <span class="hljs-string">'https://unpkg.com/'</span>, 144 <span class="hljs-string">'https://www.gstatic.com/flutter-canvaskit/'</span>, 145 ], 146 <span class="hljs-string">'script-src-elem'</span>: [ 147 <span class="hljs-string">"'self'"</span>, 148 <span class="hljs-string">'blob:'</span>, 149 <span class="hljs-string">'https://unpkg.com/'</span>, 150 <span class="hljs-string">'https://www.gstatic.com/flutter-canvaskit/'</span>, 151 ], 152 <span class="hljs-string">'connect-src'</span>: [ 153 <span class="hljs-string">"'self'"</span>, 154 <span class="hljs-string">'https://unpkg.com/'</span>, 155 <span class="hljs-string">'https://www.gstatic.com/flutter-canvaskit/'</span>, 156 <span class="hljs-string">'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf '</span>, 157 ], 158 <span class="hljs-string">'style-src'</span>: [ 159 <span class="hljs-string">"'self'"</span>, 160 <span class="hljs-string">'https:'</span>, 161 ], 162 <span class="hljs-string">'require-trusted-types-for'</span>: [<span class="hljs-string">"'script'"</span>], 163 }, 164 ), 165 ), 166 ), 167 ), 168 ); 169} 170</code></pre> 171<p>We use <code>shelf_helmet</code> to set nearly all security headers. Since we are using a Flutter Web App we have to define some Urls in our CSP. This CSP is working with CSP3 and all previous versions.</p> 172<blockquote> 173<p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" rel="noopener ugc">You can find a deeper explanation of CSP here</a></p> 174<p><a target="_blank" href="https://pub.dev/packages/shelf_helmet" rel="noopener ugc">And the documentation of shelf_helmet</a></p> 175</blockquote> 176<p>We ensure that all recommended headers are set by creating a test for our middleware. So let's create <code>test/routes/_middleware_test.dart</code>:</p> 177<pre><code class="lang-dart"><span class="hljs-keyword">
177import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 178<span class="hljs-keyword">import</span> <span class="hljs-string">'package:mocktail/mocktail.dart'</span>; 179<span class="hljs-keyword">import</span> <span class="hljs-string">'package:test/test.dart'</span>; 180<span class="hljs-keyword">import</span> <span class="hljs-string">'../../routes/_middleware.dart'</span>; 181 182<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MockRequestContext</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Mock</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">RequestContext</span> </span>{} 183 184<span class="hljs-keyword">void</span> main() { 185 group(<span class="hljs-string">'Middleware'</span>, () { 186 test(<span class="hljs-string">'add all required headers'</span>, () <span class="hljs-keyword">async</span> { 187 <span class="hljs-keyword">final</span> handler = middleware((context) => Response()); 188 <span class="hljs-keyword">final</span> request = Request.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'http://localhost/'</span>));
189 <span class="hljs-keyword">final</span> context = _MockRequestContext(); 190 191 when(() => context.request).thenReturn(request); 192 193 <span class="hljs-keyword">final</span> finishedHandler = <span class="hljs-keyword">await</span> handler(context); 194 195 <span class="hljs-keyword">const</span> cspRules = 196 <span class="hljs-string">'''script-src 'strict-dynamic' 'wasm-unsafe-eval' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https:;require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests'''</span>; 197 _expectedHeaders(finishedHandler.headers, cspRules); 198 }); 199 }); 200} 201 202<span class="hljs-keyword">void</span> _expectedHeaders(<span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>> headers, <span class="hljs-built_in">String</span> cspRules) { 203 expect(headers[<span class="hljs-string">'content-length'</span>], <span class="hljs-string">'0'</span>); 204 expect(headers[<span class="hljs-string">'x-xss-protection'</span>], <span class="hljs-string">'0'</span>); 205 expect(headers[<span class="hljs-string">'x-permitted-cross-domain-policies'</span>], <span class="hljs-string">'none'</span>); 206 expect(headers[<span class="hljs-string">'x-frame-options'</span>], <span class="hljs-string">'SAMEORIGIN'</span>); 207 expect(headers[<span class="hljs-string">'x-download-options'</span>], <span class="hljs-string">'noopen'</span>); 208 expect(headers[<span class="hljs-string">'x-dns-prefetch-control'</span>], <span class="hljs-string">'off'</span>); 209 expect(headers[<span class="hljs-string">'x-content-type-options'</span>], <span class="hljs-string">'nosniff'</span>); 210 expect( 211 headers[<span class="hljs-string">'strict-transport-security'</span>], 212 <span class="hljs-string">'max-age=15552000; includeSubDomains'</span>, 213 ); 214 expect(headers[<span class="hljs-string">'referrer-policy'</span>], <span class="hljs-string">'no-referrer'</span>); 215 expect(headers[<span class="hljs-string">'origin-agent-cluster'</span>], <span class="hljs-string">'?1'</span>); 216 expect(headers[<span class="hljs-string">'cross-origin-resource-policy'</span>], <span class="hljs-string">'same-origin'</span>); 217 expect(headers[<span class="hljs-string">'cross-origin-opener-policy'</span>], <span class="hljs-string">'same-origin'</span>); 218 expect(headers[<span class="hljs-string">'Content-Security-Policy'</span>], cspRules); 219} 220</code></pre> 221<p>In our <code>_expectedHeaders</code> we can see the work of <code>shelf_helmet</code>.</p> 222<h3 id="heading-create-hashes-for-inline-scripts-with-the-csphasher-package">Create hashes for <code>inline-scripts</code> with the <code>csp_hasher</code> package</h3> 223<p>Since Flutter is using <code>inline-scripts</code> like this:</p> 224<pre><code class="lang-xml"><span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript"> 225 <span class="hljs-comment">// The value below is injected by flutter build, do not touch.</span> 226 <span class="hljs-keyword">var</span> serviceWorkerVersion = <span class="hljs-string">"1515245720"</span>; 227</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span> 228</code></pre> 229<p>which is causing our Content Security Policy to throw errors and cause our web app to not load we have to generate Hashes for those functions.</p> 230<p>Let's start by hashing our <code>inline-scripts</code> in a custom entrypoint at our project root</p> 231<pre><code class="lang-dart"><span class="hljs-comment">// custom entrypoint for the app</span> 232<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>; 233 234<span class="hljs-keyword">
234import</span> <span class="hljs-string">'package:csp_hasher/csp_hasher.dart'</span>; 235<span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 236<span class="hljs-keyword">import</span> <span class="hljs-string">'package:path/path.dart'</span> <span class="hljs-keyword">as</span> path; 237 238<span class="hljs-built_in">List</span><CspHash> cspScriptHashes = []; 239<span class="hljs-built_in">List</span><CspHash> cspStyleHashes = []; 240 241Future<HttpServer> run(Handler handler, InternetAddress ip, <span class="hljs-built_in">int</span> port) { 242 generateCspHashes(); 243 <span class="hljs-keyword">return</span> serve(handler, ip, port, poweredByHeader: <span class="hljs-keyword">null</span>); 244} 245 246<span class="hljs-comment">/// <span class="markdown">Generates CSP hashes for the scripts and styles in the index.html file</span></span> 247<span class="hljs-keyword">void</span> generateCspHashes() { 248 <span class="hljs-keyword">final</span> file = File( 249 path.join(Directory.current.path, <span class="hljs-string">'public'</span>, <span class="hljs-string">'-.html'</span>), 250 ); 251 <span class="hljs-keyword">if</span> (!file.existsSync()) { 252 <span class="hljs-keyword">throw</span> Exception(<span class="hljs-string">'Index Not found\nPlease run build_web.sh first'</span>); 253 } 254 255 cspScriptHashes = hashScripts(htmlFile: file); 256 cspStyleHashes = hashScripts( 257 htmlFile: file, 258 hashMode: HashMode.style, 259 ); 260} 261</code></pre> 262<blockquote> 263<p><a target="_blank" href="https://pub.dev/packages/csp_hasher" rel="noopener ugc">You can find a link to the <code>csp_hasher</code> package here</a></p> 264</blockquote> 265<p>This is creating hashes for all our <code>inline-scripts</code> at every hot-reload.</p> 266<p>We can now inject the hashes into our Content Security Policy in the <code>_middleware.dart</code> file so that it looks like this:</p> 267<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 268<span class="hljs-keyword">import</span> <span class="hljs-string">'package:shelf_helmet/shelf_helmet.dart'</span>; 269 270<span class="hljs-keyword">import</span> <span class="hljs-string">'../main.dart'</span>; 271 272Handler middleware(Handler handler) { 273 <span class="hljs-keyword">return</span> handler.use(requestLogger()).use( 274 fromShelfMiddleware( 275 helmet( 276 options: HelmetOptions( 277 cspOptions: ContentSecurityPolicyOptions.useDefaults( 278 directives: { 279 <span class="hljs-string">'script-src'</span>: [ 280 <span class="hljs-string">"'strict-dynamic'"</span>, 281 <span class="hljs-string">"'wasm-unsafe-eval'"</span>, 282 cspScriptHashes.join(<span class="hljs-string">' '</span>).replaceAll(<span class="hljs-string">'"'</span>, <span class="hljs-string">''</span>), 283 <span class="hljs-string">"'self'"</span>, 284 <span class="hljs-string">'blob:'</span>, 285 <span class="hljs-string">'https://unpkg.com/'</span>, 286 <span class="hljs-string">'https://www.gstatic.com/flutter-canvaskit/'</span>, 287 ], 288 <span class="hljs-string">'script-src-elem'</span>: [ 289 cspScriptHashes.join(<span class="hljs-string">' '</span>).replaceAll(<span class="hljs-string">'"'</span>, <span class="hljs-string">''</span>), 290 <span class="hljs-string">"'self'"</span>, 291 <span class="hljs-string">'blob:'</span>, 292 <span class="hljs-string">'https://unpkg.com/'</span>, 293 <span class="hljs-string">'https://www.gstatic.com/flutter-canvaskit/'</span>, 294 ], 295 <span class="hljs-string">'connect-src'</span>: [ 296 <span class="hljs-string">"'self'"</span>, 297 <span class="hljs-string">'https://unpkg.com/'</span>, 298 <span class="hljs-string">'https://www.gstatic.com/flutter-canvaskit/'</span>,
299 <span class="hljs-string">'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf '</span>, 300 ], 301 <span class="hljs-string">'style-src'</span>: [ 302 <span class="hljs-string">"'self'"</span>, 303 <span class="hljs-string">'https:'</span>, 304 cspStyleHashes.join(<span class="hljs-string">' '</span>).replaceAll(<span class="hljs-string">'"'</span>, <span class="hljs-string">''</span>), 305 ], 306 <span class="hljs-string">'require-trusted-types-for'</span>: [<span class="hljs-string">"'script'"</span>], 307 }, 308 ), 309 ), 310 ), 311 ), 312 ); 313} 314</code></pre> 315<p>And our updated test is looking like this:</p> 316<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:csp_hasher/csp_hasher.dart'</span>; 317<span class="hljs-keyword">import</span> <span class="hljs-string">'package:dart_frog/dart_frog.dart'</span>; 318<span class="hljs-keyword">import</span> <span class="hljs-string">'package:mocktail/mocktail.dart'</span>; 319<span class="hljs-keyword">import</span> <span class="hljs-string">'package:test/test.dart'</span>; 320 321<span class="hljs-keyword">import</span> <span class="hljs-string">'../../main.dart'</span>; 322<span class="hljs-keyword">import</span> <span class="hljs-string">'../../routes/_middleware.dart'</span>; 323 324<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MockRequestContext</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Mock</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">RequestContext</span> </span>{} 325 326<span class="hljs-keyword">void</span> main() { 327 group(<span class="hljs-string">'Middleware'</span>, () { 328 setUp(() { 329 cspScriptHashes.addAll([ 330 CspHash( 331 lineNumber: <span class="hljs-number">1</span>, 332 hashType: sha256, 333 hash: <span class="hljs-string">'abcdef'</span>, 334 hashMode: HashMode.script, 335 ), 336 ]); 337 cspStyleHashes.addAll([ 338 CspHash( 339 lineNumber: <span class="hljs-number">1</span>, 340 hashType: sha256, 341 hash: <span class="hljs-string">'ghijkl'</span>, 342 hashMode: HashMode.style, 343 ), 344 ]); 345 }); 346 test(<span class="hljs-string">'add all required headers'</span>, () <span class="hljs-keyword">async</span> { 347 <span class="hljs-keyword">final</span> handler = middleware((context) => Response());
348 <span class="hljs-keyword">final</span> request = Request.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'http://localhost/'</span>)); 349 <span class="hljs-keyword">final</span> context = _MockRequestContext(); 350 351 when(() => context.request).thenReturn(request); 352 353 <span class="hljs-keyword">final</span> finishedHandler = <span class="hljs-keyword">await</span> handler(context); 354 355 <span class="hljs-keyword">const</span> cspRules = 356 <span class="hljs-string">'''script-src 'unsafe-inline' 'strict-dynamic' 'wasm-unsafe-eval' 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https: 'sha256-ghijkl';require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests'''</span>; 357 _expectedHeaders(finishedHandler.headers, cspRules); 358 }); 359 }); 360} 361 362<span class="hljs-keyword">void</span> _expectedHeaders(<span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>> headers, <span class="hljs-built_in">String</span> cspRules) { 363 expect(headers[<span class="hljs-string">'content-length'</span>], <span class="hljs-string">'0'</span>); 364 expect(headers[<span class="hljs-string">'x-xss-protection'</span>], <span class="hljs-string">'0'</span>); 365 expect(headers[<span class="hljs-string">'x-permitted-cross-domain-policies'</span>], <span class="hljs-string">'none'</span>); 366 expect(headers[<span class="hljs-string">'x-frame-options'</span>], <span class="hljs-string">'SAMEORIGIN'</span>); 367 expect(headers[<span class="hljs-string">'x-download-options'</span>], <span class="hljs-string">'noopen'</span>); 368 expect(headers[<span class="hljs-string">'x-dns-prefetch-control'</span>], <span class="hljs-string">'off'</span>); 369 expect(headers[<span class="hljs-string">'x-content-type-options'</span>], <span class="hljs-string">'nosniff'</span>); 370 expect( 371 headers[<span class="hljs-string">'strict-transport-security'</span>], 372 <span class="hljs-string">'max-age=15552000; includeSubDomains'</span>, 373 ); 374 expect(headers[<span class="hljs-string">'referrer-policy'</span>], <span class="hljs-string">'no-referrer'</span>); 375 expect(headers[<span class="hljs-string">'origin-agent-cluster'</span>], <span class="hljs-string">'?1'</span>); 376 expect(headers[<span class="hljs-string">'cross-origin-resource-policy'</span>], <span class="hljs-string">'same-origin'</span>); 377 expect(headers[<span class="hljs-string">'cross-origin-opener-policy'</span>], <span class="hljs-string">'same-origin'</span>); 378 expect(headers[<span class="hljs-string">'Content-Security-Policy'</span>], cspRules); 379} 380</code></pre> 381<h3 id="heading-conclusion">Conclusion</h3> 382<p>By utilizing <code>dart_frog</code> in combination with <code>shelf_helmet</code> and <code>csp_hasher</code> it is quite easy to serve a Flutter Web App with the recommended Security Headers and a very strong <code>Content-Security-Policy</code>. Nevertheless, we can avoid all the hashing by removing all <code>inline-scripts</code> from our <code>index.html</code></p> 383<p><a target="_blank" href="https://github.com/jxstxn1/hash_demo" rel="noopener ugc">Link to the GitHub Repo</a></p> 384</div></div></div></div><div class="max-w-2xl mx-auto border-t border-border pt-8 space-y-5"><div class="flex flex-wrap items-center gap-2"><a class="inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary" href="/tag/dart">#<!-- -->dart</a><a class="inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary" href="/tag/flutter">#<!-- -->flutter</a><a class="inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary" href="/tag/dart-language">#<!-- -->dart-language</a><a class="inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary" href="/tag/dartfrog">#<!-- -->dartfrog</a><a class="inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary" href="/tag/shelf">#<!-- -->shelf</a></div></div></div></article></div></div></div></section><section class="pb-8"><div class="px-4 sm:px-8"><div class="w-full max-w-full xl:mx-auto xl:max-w-[80rem] 2xl:max-w-[90rem]"><div class="max-w-4xl w-full mx-auto"><div class="rounded-xl border bg-card text-card-foreground p-5 sm:p-8"><section class="space-y-5"><div class="flex items-center justify-between"><h2 class="text-lg font-bold tracking-tight">Comments<!-- --> (1)</h2><a target="_blank" rel="noopener" class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition hover:border-primary hover:text-primary" href="https://hashnode.com/posts/elevate-your-flutter-web-hosting-with-dartfrog/64d5f9d6e6424c168da70ac5"><i class="fa-regular fa-comment" aria-hidden="true"></i>Join the discussion</a></div><div class="space-y-5"><div class="space-y-3"><div class=""><div class="flex gap-3"><a target="_blank" rel="noopener" class="shrink-0" href="https://hashnode.com/@kdhair14"><span class="relative flex shrink-0 overflow-hidden rounded-full h-8 w-8 border border-border transition-opacity hover:opacity-80"><span class="flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground font-semibold text-sm">D</span></span></a><div class="min-w-0 flex-1"><div class="flex items-center gap-2 text-sm"><a target="_blank" rel="noopener" class="font-medium text-foreground hover:text-primary transition-colors" href="https://hashnode.com/@kdhair14">
384Dhairya Kubadia</a><span class="text-muted-foreground">3y ago</span></div><div class="mt-1 text-sm text-foreground prose dark:prose-invert prose-sm max-w-none [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0"><p>Very Informative!</p> 385</div></div></div></div></div></div></section></div></div></div></div></section>
385<script type="application/ld+json">{"@context":"https://schema.org","@type":"Article","url":"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog","mainEntityOfPage":"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog","headline":"Elevate Your Flutter Web Hosting with Dart_Frog","description":"Link to the GitHub Repo\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...","author":{"@type":"Person","name":"Justin Baumann","url":"https://hashnode.com/@jxstxn1","image":"https://cdn.hashnode.com/res/hashnode/image/upload/v1670325403203/m8iKNQWRdC.jpeg","sameAs":"https://x.com/jxstxn_____"},"publisher":{"@type":"Organization","name":"jxstxn","url":"https://jxstxn.dev"},"datePublished":"2023-08-11T09:05:26.899Z","dateModified":"2023-08-11T09:05:26.899Z","image":{"@type":"ImageObject","url":"https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png"},"keywords":"Dart, Flutter, #dart language, dart_frog, shelf"}</script>
385<footer class="px-4 pt-8 pb-20 sm:px-8 text-muted-foreground" aria-label="Footer"><div class="w-full max-w-4xl mx-auto"><div class="flex flex-col items-center gap-4 text-center text-sm"><p class="font-medium"><time dateTime="2026"><span class="font-sans">©</span> <!-- -->2026</time> <!-- -->jxstxn</p><nav aria-label="Footer navigation"><ul class="flex flex-wrap justify-center gap-4 text-sm tracking-wide"><li><a class="transition hover:text-foreground" href="/members">Members</a></li><li><a class="transition hover:text-foreground" href="/archive">Archive</a></li><li><a class="transition hover:text-foreground" target="_blank" rel="noopener" href="https://hashnode.com/privacy-policy">Privacy</a></li><li><a class="transition hover:text-foreground" target="_blank" rel="noopener" href="https://hashnode.com/terms">Terms</a></li></ul></nav><div class="pt-8 flex justify-center gap-6"><a target="_blank" rel="noopener" class="flex items-center gap-2 text-sm transition hover:text-foreground" title="Sitemap" href="/sitemap.xml"><i class="fa-regular fa-sitemap"></i><span>Sitemap</span></a><a target="_blank" rel="noopener" class="flex items-center gap-2 text-sm transition hover:text-foreground" title="RSS Feed" href="/rss.xml"><i class="fa-regular fa-rss"></i><span>RSS</span></a></div><div class="pt-10 text-center"><a target="_blank" rel="noopener" aria-label="Visit Hashnode (opens in a new tab)" class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-10 rounded-md text-xl px-5 py-4" href="https://hashnode.com/?utm_source=https%3A%2F%2Fjxstxn.dev&utm_medium=referral&utm_campaign=blog_footer_logo&utm_content=logo"><svg fill="none" viewBox="0 0 282 39" aria-hidden="true" class="h-6 w-auto min-w-[9.5rem] text-muted-foreground transition hover:text-foreground"><path fill="currentColor" d="M2.68 13.032c-3.573 3.505-3.573 9.363 0 12.936L13.032 36.32c3.505 3.573 9.363 3.573 12.936 0L36.32 25.968c3.573-3.573 3.573-9.431 0-12.936L25.968 2.68c-3.573-3.573-9.431-3.573-12.936 0zm12.211 1.935c2.507-2.521 6.582-2.544 9.104-.038s2.544 6.582.038 9.104-6.582 2.544-9.104.038-2.544-6.582-.038-9.104"></path><path fill="currentColor" d="M268.379 39q-6.322 0-10.35-4.108-4.002-4.134-4.002-10.483 0-6.296 4.002-10.43 4-4.135 10.163-4.135 6.028 0 9.817 3.734 3.814 3.735 3.814 9.497v2.8h-24.248v-4.054h17.633q-.187-2.747-2.054-4.508-1.868-1.76-4.962-1.76-3.28-.001-5.388 2.374-2.108 2.373-2.108 6.375 0 4.188 2.348 6.695 2.347 2.481 5.895 2.481 1.68 0 2.934-.453a8.5 8.5 0 0 0 2.321-1.307q1.041-.854 2.268-2.614l4.694 2.96q-1.707 2.748-3.494 4.135a12.7 12.7 0 0 1-3.975 2.081q-2.187.72-5.308.72m-33.496 0q-5.575 0-9.283-4.028-3.681-4.029-3.681-10.484 0-6.43 3.761-10.51 3.76-4.108 9.47-4.108 2.8 0 5.175 1.2 2.4 1.174 3.628 3.148l.026-.08V0h7.016v38.2h-6.722v-4.722l.64.8-.694.133q-1.28 2.108-3.788 3.361Q237.924 39 234.883 39m1.627-6.002q3.308 0 5.495-2.428t2.188-6.135q0-3.762-2.214-6.109-2.214-2.374-5.469-2.374-3.28 0-5.522 2.347-2.214 2.348-2.214 6.163 0 3.76 2.214 6.162 2.214 2.374 5.522 2.374M205.811 39q-6.376 0-10.537-4.161-4.161-4.188-4.161-10.404 0-6.321 4.161-10.456 4.188-4.135 10.537-4.135 6.348 0 10.537 4.161 4.188 4.161 4.188 10.43 0 6.216-4.188 10.404Q212.186 39 205.811 39m0-6.162q3.307 0 5.575-2.374 2.267-2.374 2.267-6.055 0-3.707-2.294-6.056-2.267-2.347-5.548-2.347-3.308 0-5.575 2.374-2.268 2.348-2.268 6.029t2.241 6.055q2.268 2.374 5.602 2.374M162.586 38.2V10.644h6.802v5.015l-.8-.987h.906q1.308-2.187 3.762-3.494 2.48-1.335 5.228-1.334 4.615 0 7.256 2.8 2.667 2.776 2.667 7.603V38.2h-7.069V21.794q0-2.854-1.387-4.401t-4.134-1.547q-2.614 0-4.402 1.974-1.76 1.974-1.76 4.881V38.2zm-30.181 0V0h6.962v14.698h.134q1.2-2.187 3.574-3.521 2.375-1.335 5.229-1.334 4.615 0 7.255 2.828 2.668 2.801 2.668 7.522V38.2h-7.069V21.794q0-2.827-1.387-4.402-1.387-1.573-4.055-1.573-2.587 0-4.428 2.027-1.814 2.027-1.814 4.962v15.391zm-14.416.8q-2.882 0-5.015-.64-2.108-.614-3.761-1.84-1.654-1.228-2.668-2.695l-.987-1.467 4.695-3.174.773 1.067a9.1 9.1 0 0 0 1.788 1.814 8.2 8.2 0 0 0 2.32 1.2q1.308.4 3.068.4 2.161 0 3.441-.8 1.308-.8 1.307-2.294 0-1.36-1.147-2.108-1.12-.747-5.068-1.654-5.335-1.253-7.656-3.334-2.32-2.107-2.321-5.362 0-3.76 3.148-6.002 3.148-2.267 7.923-2.267 2.454 0 4.348.507t3.388 1.493q1.493.988 2.454 2.188l.987 1.2-4.482 3.255-.667-.774a7.4 7.4 0 0 0-1.627-1.387 6.4 6.4 0 0 0-2.054-.88 9.5 9.5 0 0 0-2.401-.294q-1.974 0-3.201.8-1.2.774-1.2 1.921 0 1.254 1.12 2.027 1.147.748 5.229 1.681 5.334 1.227 7.629 3.308 2.294 2.055 2.294 5.308 0 4.029-3.228 6.43Q123.217 39 117.989 39M87.61 39q-4.508 0-7.335-2.454-2.828-2.481-2.828-6.482 0-4.188 3.254-6.669t8.483-2.48q2.082 0 4.028.426 1.974.427 3.361 1.12v-1.6q0-2.588-1.813-4.082-1.788-1.493-4.615-1.493-1.628 0-2.801.373a8.7 8.7 0 0 0-2.188.987q-.987.614-1.84 1.414l-.854.8-4.081-3.788 1.307-1.173a18.7 18.7 0 0 1 2.934-2.108q1.601-.933 3.601-1.44t4.589-.507q5.868 0 9.229 2.88 3.361 2.882 3.361 8.084V38.2h-6.749v-5.922l1.654 2.187h-1.76q-1.308 2.055-3.655 3.308Q90.545 39 87.611 39m1.814-4.988q3.148 0 5.149-2.187 2-2.215 2-5.309v-.08q-1.12-.693-2.72-1.094a13 13 0 0 0-3.388-.426q-2.828 0-4.562 1.2-1.734 1.173-1.734 3.521 0 2.028 1.468 3.201t3.787 1.174M49 38.2V0h6.962v14.698h.134q1.2-2.187 3.574-3.521 2.375-1.335 5.229-1.334 4.615 0 7.255 2.828 2.668 2.801 2.668 7.522V38.2h-
3857.07V21.794q0-2.827-1.386-4.402-1.387-1.573-4.055-1.573-2.587 0-4.428 2.027-1.815 2.027-1.814 4.962v15.391z"></path></svg></a></div></div></div></footer></main><!--$--><!--/$-->
385<script src="/_next/static/chunks/webpack-55b36f0cd6c208fe.js" id="_R_" async=""></script>
385<script>(self.__next_f=self.__next_f||[]).push([0])</script>
385<script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[16174,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"2810\",\"static/chunks/2810-6b74d77b4b0dc790.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"7177\",\"static/chunks/app/layout-6ae7b4b583d4bba3.js\"],\"ThemeProvider\"]\n3:I[82180,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"2810\",\"static/chunks/2810-6b74d77b4b0dc790.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"7177\",\"static/chunks/app/layout-6ae7b4b583d4bba3.js\"],\"TooltipProvider\"]\n4:I[63851,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"2810\",\"static/chunks/2810-6b74d77b4b0dc790.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"7177\",\"static/chunks/app/layout-6ae7b4b583d4bba3.js\"],\"UrqlProvider\"]\n5:I[82714,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"2810\",\"static/chunks/2810-6b74d77b4b0dc790.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"7177\",\"static/chunks/app/layout-6ae7b4b583d4bba3.js\"],\"ConsentManager\"]\n6:I[9766,[],\"\"]\n7:I[98924,[],\"\"]\n8:I[33293,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"4345\",\"static/chunks/app/not-found-b861935655c5698b.js\"],\"default\"]\n9:I[21447,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"2810\",\"static/chunks/2810-6b74d77b4b0dc790.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"7177\",\"static/chunks/app/layout-6ae7b4b583d4bba3.js\"],\"Integrations\"]\na:I[35514,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"2810\",\"static/chunks/2810-6b74d77b4b0dc790.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"7177\",\"static/chunks/app/layout-6ae7b4b583d4bba3.js\"],\"Analytics\"]\nc:I[24431,[],\"OutletBoundary\"]\ne:I[15278,[],\"AsyncMetadataOutlet\"]\n10:I[24431,[],\"ViewportBoundary\"]\n12:I[24431,[],\"MetadataBoundary\"]\n13:\"$Sreact.suspense\"\n15:I[57150,[],\"\"]\n:HL[\"/_next/static/media/22a5144ee8d83bca-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/72a2a501b11d0ccb.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/8934b430a4adeb19-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/8e9860b6e62d6359-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/c4b700dcb2187787-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/0d16c72050851854.css\",\"style\"]\n:HL[\"/_next/static/css/7ffcc02a3c67389b.css\",\"style\"]\n:HL[\"/_next/static/css/c20e76e7d11dec32.css\",\"style\"]\n:HL[\"/_next/static/css/59fefb2212bd5b60.css\",\"style\"]\n0:{\"P\":null,\"b\":\"RrCGzNVHUG674hJ_sjvms\",\"p\":\"\",\"c\":[\"\",\"elevate-your-flutter-web-hosting-with-dartfrog?x-host=jxstxn.dev\"],\"i\":false,\"f\":[[[\"\",{\"children\":[[\"slug\",\"elevate-your-flutter-web-hosting-with-dartfrog\",\"c\"],{\"children\":[\"__PAGE__?{\\\"x-host\\\":\\\"jxstxn.dev\\\"}\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/0d16c72050851854.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/7ffcc02a3c67389b.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"2\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/c20e76e7d11dec32.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"link\",\"3\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/59fefb2212bd5b60.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"s"])</script>
385<script>self.__next_f.push([1,"uppressHydrationWarning\":true,\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"type\":\"text/javascript\",\"dangerouslySetInnerHTML\":{\"__html\":\"window.addEventListener('message',function(e){var d=e.data;if(!d||d.__hnWidget!==1||typeof d.h!=='number'||d.h\u003c=0)return;var f=document.querySelectorAll('.hn-widget-frame');for(var i=0;i\u003cf.length;i++){if(f[i].contentWindow===e.source){f[i].style.height=d.h+'px';break;}}});\"}}]}],[\"$\",\"body\",null,{\"className\":\"__variable_c2612a __variable_246ccd __variable_3069d5 bg-background dark:bg-background\",\"children\":[\"$\",\"$L2\",null,{\"attribute\":\"class\",\"defaultTheme\":\"system\",\"enableSystem\":true,\"disableTransitionOnChange\":true,\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$L5\",null,{\"children\":[[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"$L8\",null,{}],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}],[\"$\",\"$L9\",null,{}],[\"$\",\"$La\",null,{}]]}]}]}]}]}]]}]]}],{\"children\":[[\"slug\",\"elevate-your-flutter-web-hosting-with-dartfrog\",\"c\"],[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[\"$Lb\",null,[\"$\",\"$Lc\",null,{\"children\":[\"$Ld\",[\"$\",\"$Le\",null,{\"promise\":\"$@f\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$L10\",null,{\"children\":\"$L11\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$L12\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$13\",null,{\"fallback\":null,\"children\":\"$L14\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$15\",[]],\"s\":false,\"S\":false}\n11:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nd:null\n16:I[80622,[],\"IconMark\"]\nf:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Elevate Your Flutter Web Hosting with Dart_Frog\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\"}],[\"$\",\"link\",\"2\",{\"rel\":\"canonical\",\"href\":\"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog\"}],[\"$\",\"meta\",\"3\",{\"property\":\"og:title\",\"content\":\"Elevate Your Flutter Web Hosting with Dart_Frog\"}],[\"$\",\"meta\",\"4\",{\"property\":\"og:description\",\"content\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\"}],[\"$\",\"meta\",\"5\",{\"property\":\"og:url\",\"content\":\"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog\"}],[\"$\",\"meta\",\"6\",{\"property\":\"og:site_name\",\"content\":\"jxstxn\"}],[\"$\",\"meta\",\"7\",{\"property\":\"og:image\",\"content\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691744613789/9a5f451a-bf52-459e-8b99-c91e5e592469.png\"}],[\"$\",\"meta\",\"8\",{\"property\":\"og:image:width\",\"content\":\"1200\"}],[\"$\",\"meta\",\"9\",{\"property\":\"og:image:height\",\"content\":\"630\"}],[\"$\",\"meta\",\"10\",{\"property\":\"og:type\",\"content\":\"article\"}],[\"$\",\"meta\",\"11\",{\"property\":\"article:published_time\",\"content\":\"2023-08-11T09:05:26.899Z\"}],[\"$\",\"meta\",\"12\",{\"property\":\"article:author\",\"content\":\"https://jxstxn.dev/@jxstxn1\"}],[\"$\",\"meta\",\"13\",{\"name\":\"twitter:card\",\"content\":\"summary_large_image\"}],[\"$\",\"meta\",\"14\",{\"name\":\"twitter:creator\",\"content\":\"https://twitter.com/jxstxn_____\"}],[\"$\",\"meta\",\"15\",{\"name\":\"twitter:title\",\"content\":\"Elevate Your Flutter Web Hosting wit"])</script>
385<script>self.__next_f.push([1,"h Dart_Frog\"}],[\"$\",\"meta\",\"16\",{\"name\":\"twitter:description\",\"content\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\"}],[\"$\",\"meta\",\"17\",{\"name\":\"twitter:image\",\"content\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691744613789/9a5f451a-bf52-459e-8b99-c91e5e592469.png\"}],[\"$\",\"link\",\"18\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\"}],[\"$\",\"$L16\",\"19\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n14:\"$f:metadata\"\n17:I[652,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"8974\",\"static/chunks/app/page-cc18ae1a3a7a5ed8.js\"],\"BlogHeader\"]\n18:T6884,\u003cp\u003e\u003ca target=\"_blank\" href=\"https://github.com/jxstxn1/hash_demo\"\u003eLink to the GitHub Repo\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the following:\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691525897533/5137f6e7-1424-4949-bdf9-352276200e3e.png\" alt class=\"image--center mx-auto\" /\u003e\u003c/p\u003e\n\u003cp\u003eThis can be very frustrating especially if you start whitelisting scripts to your Content Security Policy and those scripts are requesting resources from somewhere else.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSo what's the solution for this?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eContent Security Policy errors can only be solved by the web server which is hosting the Web App. There are probably multiple solutions to tackle this problem, but today we are going to take a deep dive into \u003ccode\u003edart_frog\u003c/code\u003e and how to utilize it to host our Flutter Web App with some advanced capabilities.\u003c/p\u003e\n\u003ch3 id=\"heading-key-topics\"\u003eKey Topics\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eNote: Content Security Policy will be called CSP in this Blog Post\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cp\u003eServe a Flutter Web App with \u003ccode\u003edart_frog\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eSet all recommended security headers with \u003ccode\u003eshelf_helmet\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eCreate a Content Security Policy that works with CSP2 and 3\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eCreate Hashes for \u003ccode\u003einline-scripts\u003c/code\u003e to whitelist them in our Content Security Policy\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"heading-setting-everything-up\"\u003eSetting everything up\u003c/h3\u003e\n\u003cp\u003eWe'll start by creating a brand new dart_frog project\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003edart_frog create hash_demo\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWe can then open our \u003ccode\u003epubpsec.yaml\u003c/code\u003e and replace the contents with\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003e\u003cspan class=\"hljs-attr\"\u003ename:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ehash_demo\u003c/span\u003e\n\u003cspan class=\"hljs-attr\"\u003edescription:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eAn\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eexample\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eof\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ehow\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eto\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003euse\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ethe\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eCSP\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eHasher\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003epackage\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ein\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ecombination\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ewith\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eshelf_helmet.\u003c/span\u003e\n\u003cspan class=\"hljs-attr\"\u003eversion:\u003c/span\u003e \u003cspan class=\"hljs-number\"\u003e1.0\u003c/span\u003e\u003cspan class=\"hljs-number\"\u003e.0\u003c/span\u003e\u003cspan class=\"hljs-string\"\u003e+1\u003c/span\u003e\n\u003cspan class=\"hljs-attr\"\u003epublish_to:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003enone\u003c/span\u003e\n\n\u003cspan class=\"hljs-attr\"\u003eenvironment:\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003esdk:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e\"\u0026gt;=3.0.0 \u0026lt;4.0.0\"\u003c/span\u003e\n\n\u003cspan class=\"hljs-attr\"\u003edependencies:\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003ecsp_hasher:\u003c/span\u003e \u003cspan class=\""])</script>
385<script>self.__next_f.push([1,"hljs-string\"\u003e^1.0.0\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003edart_frog:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.0.0\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003epath:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.8.3\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003eshelf_helmet:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^2.1.1\u003c/span\u003e\n\n\u003cspan class=\"hljs-attr\"\u003edev_dependencies:\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003emocktail:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^0.3.0\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003etest:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.19.2\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003every_good_analysis:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^5.0.0\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNow we have to create our Flutter project with:\u003cbr /\u003e\u003ccode\u003eflutter create counter --platform web\u003c/code\u003e\u003c/p\u003e\n\u003ch3 id=\"heading-build-and-copy-the-flutter-app\"\u003eBuild and copy the Flutter App\u003c/h3\u003e\n\u003cp\u003eTo build our Flutter App we run in \u003ccode\u003ecounter\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003eflutter build web --web-renderer canvaskit --release --csp\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAfter the build, we can copy the output from \u003ccode\u003ecounter/build/web\u003c/code\u003e to \u003ccode\u003epublic/\u003c/code\u003e\u003cbr /\u003eTo simplify this you can run from \u003ccode\u003ehash_demo\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003ecp -r counter/build/web/ public/\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"heading-serve-flutter-app-at\"\u003eServe Flutter App at /\u003c/h3\u003e\n\u003cp\u003eTo serve the \u003ccode\u003eindex.html\u003c/code\u003e file directly at the \u003ccode\u003e/\u003c/code\u003e location we modify the \u003ccode\u003eroutes/index.dart\u003c/code\u003e file with:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'dart:io'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:path/path.dart'\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eas\u003c/span\u003e path;\n\nResponse onRequest(RequestContext context) {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e file = File(\n path.join(Directory.current.path, \u003cspan class=\"hljs-string\"\u003e'public'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e'index.html'\u003c/span\u003e),\n );\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e indexHtml = file.readAsStringSync();\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e Response(body: indexHtml, headers: {\u003cspan class=\"hljs-string\"\u003e'Content-Type'\u003c/span\u003e: \u003cspan class=\"hljs-string\"\u003e'text/html'\u003c/span\u003e});\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTo ensure that everything works as expected we will write a test for it.\u003cbr /\u003eLet's create \u003ccode\u003etest/routes/index_test.dart\u003c/code\u003e :\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'dart:io'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:mocktail/mocktail.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:test/test.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../routes/index.dart'\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eas\u003c/span\u003e route;\n\n\u003cspan class=\"hljs-class\"\u003e\u003cspan class=\"hljs-keyword\"\u003eclass\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003e_MockRequestContext\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eextends\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eMock\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eimplements\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eRequestContext\u003c/span\u003e \u003c/span\u003e{}\n\n\u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e htmlString = File(\n \u003cspan class=\"hljs-string\"\u003e'\u003cspan class=\"hljs-subst\"\u003e${Directory.current.path}\u003c/span\u003e/public/index.html'\u003c/span\u003e,\n).readAsStringSync();\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e main() {\n group(\u003cspan class=\"hljs-string\"\u003e'GET /'\u003c/span\u003e, () {\n test(\u003cspan class=\"hljs-string\"\u003e'responds with a 200, an html and the content_type header text/html'\u003c/span\u003e,\n () \u003cspan class=\"hljs-keyword\"\u003easync\u003c/span\u003e {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e context = _MockRequestContext();\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e response = route.onRequest(context);\n expect(response.statusCode, equals(\u003cspan class=\"hljs-number\"\u003e200\u003c/span\u003e));\n expect(\n response.headers,\n equals({\n \u003cspan class=\"hlj"])</script>
385<script>self.__next_f.push([1,"s-string\"\u003e'Content-Type'\u003c/span\u003e: \u003cspan class=\"hljs-string\"\u003e'text/html'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'content-length'\u003c/span\u003e: \u003cspan class=\"hljs-string\"\u003e'1830'\u003c/span\u003e,\n }),\n );\n expect(response.body(), completion(htmlString));\n });\n });\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePerfect! We can now serve our Flutter Web App at \u003ccode\u003elocalhost:8080/\u003c/code\u003e\u003cbr /\u003eTo test it in a browser you can run:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003edart_frog dev\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"heading-creating-our-middleware\"\u003eCreating our middleware\u003c/h3\u003e\n\u003cp\u003eWe can now create our middleware where most of the magic is happening.\u003cbr /\u003eTo do so run:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003edart_frog new middleware /\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWe can now replace the content in the newly created middleware with this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:shelf_helmet/shelf_helmet.dart'\u003c/span\u003e;\n\nHandler middleware(Handler handler) {\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e handler.use(requestLogger()).use(\n fromShelfMiddleware(\n helmet(\n options: \u003cspan class=\"hljs-keyword\"\u003econst\u003c/span\u003e HelmetOptions(\n cspOptions: ContentSecurityPolicyOptions.useDefaults(\n directives: {\n \u003cspan class=\"hljs-string\"\u003e'script-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'strict-dynamic'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e\"'wasm-unsafe-eval'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'script-src-elem'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'connect-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf '\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'style-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https:'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'require-trusted-types-for'\u003c/span\u003e: [\u003cspan class=\"hljs-string\"\u003e\"'script'\"\u003c/span\u003e],\n },\n ),\n ),\n ),\n ),\n );\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWe use \u003ccode\u003eshelf_helmet\u003c/code\u003e to set nearly all security headers. Since we are using a Flutter Web App we have to define some Urls in our CSP. This CSP is working with CSP3 and all previous versions.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP\"\u003eYou can find a deeper explanation of CSP here\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://pub.dev/packages/shelf_helmet\"\u003eAnd the documentation of shelf_helmet\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWe ensure that all recommended headers are set by creating a test for our middleware. So let's create \u003ccode\u003etest/routes/_middleware_test.dart\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:mocktail/mocktail.dart'\u003c/span\u003e;\n\u003cspa"])</script>
385<script>self.__next_f.push([1,"n class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:test/test.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../routes/_middleware.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-class\"\u003e\u003cspan class=\"hljs-keyword\"\u003eclass\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003e_MockRequestContext\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eextends\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eMock\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eimplements\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eRequestContext\u003c/span\u003e \u003c/span\u003e{}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e main() {\n group(\u003cspan class=\"hljs-string\"\u003e'Middleware'\u003c/span\u003e, () {\n test(\u003cspan class=\"hljs-string\"\u003e'add all required headers'\u003c/span\u003e, () \u003cspan class=\"hljs-keyword\"\u003easync\u003c/span\u003e {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e handler = middleware((context) =\u0026gt; Response());\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e request = Request.\u003cspan class=\"hljs-keyword\"\u003eget\u003c/span\u003e(\u003cspan class=\"hljs-built_in\"\u003eUri\u003c/span\u003e.parse(\u003cspan class=\"hljs-string\"\u003e'http://localhost/'\u003c/span\u003e));\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e context = _MockRequestContext();\n\n when(() =\u0026gt; context.request).thenReturn(request);\n\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e finishedHandler = \u003cspan class=\"hljs-keyword\"\u003eawait\u003c/span\u003e handler(context);\n\n \u003cspan class=\"hljs-keyword\"\u003econst\u003c/span\u003e cspRules =\n \u003cspan class=\"hljs-string\"\u003e'''script-src 'strict-dynamic' 'wasm-unsafe-eval' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https:;require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests'''\u003c/span\u003e;\n _expectedHeaders(finishedHandler.headers, cspRules);\n });\n });\n}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e _expectedHeaders(\u003cspan class=\"hljs-built_in\"\u003eMap\u003c/span\u003e\u0026lt;\u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e\u0026gt; headers, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e cspRules) {\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'content-length'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-xss-protection'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-permitted-cross-domain-policies'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'none'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-frame-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'SAMEORIGIN'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-download-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'noopen'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-dns-prefetch-control'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'off'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-content-type-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'nosniff'\u003c/span\u003e);\n expect(\n headers[\u003cspan class=\"hljs-string\"\u003e'strict-transport-security'\u003c/span\u003e],\n \u003cspan class=\"hljs-string\"\u003e'max-age=15552000; includeSubDomains'\u003c/span\u003e,\n );\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'referrer-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'no-referrer'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'origin-agent-cluster'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'?1'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-resource-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-opener-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'Content-Security-Policy'\u003c/span\u003e], cspRules);\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIn our \u003ccode\u003e_expectedHeaders\u003c/code\u003e we can see the work of \u003ccode\u003eshelf_helmet\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"heading-create-hashes-for-inline-scripts-with-the-csphasher-packag"])</script>
385<script>self.__next_f.push([1,"e\"\u003eCreate hashes for \u003ccode\u003einline-scripts\u003c/code\u003e with the \u003ccode\u003ecsp_hasher\u003c/code\u003e package\u003c/h3\u003e\n\u003cp\u003eSince Flutter is using \u003ccode\u003einline-scripts\u003c/code\u003e like this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-xml\"\u003e\u003cspan class=\"hljs-tag\"\u003e\u0026lt;\u003cspan class=\"hljs-name\"\u003escript\u003c/span\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"javascript\"\u003e\n \u003cspan class=\"hljs-comment\"\u003e// The value below is injected by flutter build, do not touch.\u003c/span\u003e\n \u003cspan class=\"hljs-keyword\"\u003evar\u003c/span\u003e serviceWorkerVersion = \u003cspan class=\"hljs-string\"\u003e\"1515245720\"\u003c/span\u003e;\n\u003c/span\u003e\u003cspan class=\"hljs-tag\"\u003e\u0026lt;/\u003cspan class=\"hljs-name\"\u003escript\u003c/span\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ewhich is causing our Content Security Policy to throw errors and cause our web app to not load we have to generate Hashes for those functions.\u003c/p\u003e\n\u003cp\u003eLet's start by hashing our \u003ccode\u003einline-scripts\u003c/code\u003e in a custom entrypoint at our project root\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-comment\"\u003e// custom entrypoint for the app\u003c/span\u003e\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'dart:io'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:csp_hasher/csp_hasher.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:path/path.dart'\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eas\u003c/span\u003e path;\n\n\u003cspan class=\"hljs-built_in\"\u003eList\u003c/span\u003e\u0026lt;CspHash\u0026gt; cspScriptHashes = [];\n\u003cspan class=\"hljs-built_in\"\u003eList\u003c/span\u003e\u0026lt;CspHash\u0026gt; cspStyleHashes = [];\n\nFuture\u0026lt;HttpServer\u0026gt; run(Handler handler, InternetAddress ip, \u003cspan class=\"hljs-built_in\"\u003eint\u003c/span\u003e port) {\n generateCspHashes();\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e serve(handler, ip, port, poweredByHeader: \u003cspan class=\"hljs-keyword\"\u003enull\u003c/span\u003e);\n}\n\n\u003cspan class=\"hljs-comment\"\u003e/// \u003cspan class=\"markdown\"\u003eGenerates CSP hashes for the scripts and styles in the index.html file\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e generateCspHashes() {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e file = File(\n path.join(Directory.current.path, \u003cspan class=\"hljs-string\"\u003e'public'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e'-.html'\u003c/span\u003e),\n );\n \u003cspan class=\"hljs-keyword\"\u003eif\u003c/span\u003e (!file.existsSync()) {\n \u003cspan class=\"hljs-keyword\"\u003ethrow\u003c/span\u003e Exception(\u003cspan class=\"hljs-string\"\u003e'Index Not found\\nPlease run build_web.sh first'\u003c/span\u003e);\n }\n\n cspScriptHashes = hashScripts(htmlFile: file);\n cspStyleHashes = hashScripts(\n htmlFile: file,\n hashMode: HashMode.style,\n );\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://pub.dev/packages/csp_hasher\"\u003eYou can find a link to the \u003ccode\u003ecsp_hasher\u003c/code\u003e package here\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis is creating hashes for all our \u003ccode\u003einline-scripts\u003c/code\u003e at every hot-reload.\u003c/p\u003e\n\u003cp\u003eWe can now inject the hashes into our Content Security Policy in the \u003ccode\u003e_middleware.dart\u003c/code\u003e file so that it looks like this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:shelf_helmet/shelf_helmet.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../main.dart'\u003c/span\u003e;\n\nHandler middleware(Handler handler) {\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e handler.use(requestLogger()).use(\n fromShelfMiddleware(\n helmet(\n options: HelmetOptions(\n cspOptions: ContentSecurityPolicyOptions.useDefaults(\n directives: {\n \u003cspan class=\"hljs-string\"\u003e'script-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'strict-dynamic'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e\"'wasm-unsafe-eval'\"\u003c/span\u003e,\n cspScriptHashes.join(\u003cspan class=\"hljs-string\"\u003e' '\u003c/span\u003e).replaceAll(\u003cspan class=\"hljs-string\"\u003e'\"'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e''\u003c/span\u003e),\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'"])</script>
385<script>self.__next_f.push([1,"blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'script-src-elem'\u003c/span\u003e: [\n cspScriptHashes.join(\u003cspan class=\"hljs-string\"\u003e' '\u003c/span\u003e).replaceAll(\u003cspan class=\"hljs-string\"\u003e'\"'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e''\u003c/span\u003e),\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'connect-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf '\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'style-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https:'\u003c/span\u003e,\n cspStyleHashes.join(\u003cspan class=\"hljs-string\"\u003e' '\u003c/span\u003e).replaceAll(\u003cspan class=\"hljs-string\"\u003e'\"'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e''\u003c/span\u003e),\n ],\n \u003cspan class=\"hljs-string\"\u003e'require-trusted-types-for'\u003c/span\u003e: [\u003cspan class=\"hljs-string\"\u003e\"'script'\"\u003c/span\u003e],\n },\n ),\n ),\n ),\n ),\n );\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAnd our updated test is looking like this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:csp_hasher/csp_hasher.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:mocktail/mocktail.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:test/test.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../main.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../routes/_middleware.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-class\"\u003e\u003cspan class=\"hljs-keyword\"\u003eclass\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003e_MockRequestContext\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eextends\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eMock\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eimplements\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eRequestContext\u003c/span\u003e \u003c/span\u003e{}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e main() {\n group(\u003cspan class=\"hljs-string\"\u003e'Middleware'\u003c/span\u003e, () {\n setUp(() {\n cspScriptHashes.addAll([\n CspHash(\n lineNumber: \u003cspan class=\"hljs-number\"\u003e1\u003c/span\u003e,\n hashType: sha256,\n hash: \u003cspan class=\"hljs-string\"\u003e'abcdef'\u003c/span\u003e,\n hashMode: HashMode.script,\n ),\n ]);\n cspStyleHashes.addAll([\n CspHash(\n lineNumber: \u003cspan class=\"hljs-number\"\u003e1\u003c/span\u003e,\n hashType: sha256,\n hash: \u003cspan class=\"hljs-string\"\u003e'ghijkl'\u003c/span\u003e,\n hashMode: HashMode.style,\n ),\n ]);\n });\n test(\u003cspan class=\"hljs-string\"\u003e'add all required headers'\u003c/span\u003e, () \u003cspan class=\"hljs-keyword\"\u003easync\u003c/span\u003e {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e handler = middleware((context) =\u0026gt; Response());\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e request = Request.\u003cspan class=\"hljs-keyword\"\u003eget\u003c/span\u003e(\u003cspan class=\"hljs-built_in\"\u003eUri\u003c/span\u003e.parse(\u003cspan class=\"hljs-string\"\u003e'http://localhost/'\u003c/span\u003e));\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e context = _MockRequestContext();\n\n when(() =\u0026gt; context.request).thenReturn(request);\n\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e finishedHandler = \u003cspan class="])</script>
385<script>self.__next_f.push([1,"\"hljs-keyword\"\u003eawait\u003c/span\u003e handler(context);\n\n \u003cspan class=\"hljs-keyword\"\u003econst\u003c/span\u003e cspRules =\n \u003cspan class=\"hljs-string\"\u003e'''script-src 'unsafe-inline' 'strict-dynamic' 'wasm-unsafe-eval' 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https: 'sha256-ghijkl';require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests'''\u003c/span\u003e;\n _expectedHeaders(finishedHandler.headers, cspRules);\n });\n });\n}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e _expectedHeaders(\u003cspan class=\"hljs-built_in\"\u003eMap\u003c/span\u003e\u0026lt;\u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e\u0026gt; headers, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e cspRules) {\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'content-length'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-xss-protection'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-permitted-cross-domain-policies'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'none'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-frame-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'SAMEORIGIN'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-download-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'noopen'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-dns-prefetch-control'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'off'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-content-type-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'nosniff'\u003c/span\u003e);\n expect(\n headers[\u003cspan class=\"hljs-string\"\u003e'strict-transport-security'\u003c/span\u003e],\n \u003cspan class=\"hljs-string\"\u003e'max-age=15552000; includeSubDomains'\u003c/span\u003e,\n );\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'referrer-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'no-referrer'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'origin-agent-cluster'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'?1'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-resource-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-opener-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'Content-Security-Policy'\u003c/span\u003e], cspRules);\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"heading-conclusion\"\u003eConclusion\u003c/h3\u003e\n\u003cp\u003eBy utilizing \u003ccode\u003edart_frog\u003c/code\u003e in combination with \u003ccode\u003eshelf_helmet\u003c/code\u003e and \u003ccode\u003ecsp_hasher\u003c/code\u003e it is quite easy to serve a Flutter Web App with the recommended Security Headers and a very strong \u003ccode\u003eContent-Security-Policy\u003c/code\u003e. Nevertheless, we can avoid all the hashing by removing all \u003ccode\u003einline-scripts\u003c/code\u003e from our \u003ccode\u003eindex.html\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://github.com/jxstxn1/hash_demo\"\u003eLink to the GitHub Repo\u003c/a\u003e\u003c/p\u003e\n19:T39be,[Link to the GitHub Repo](https://github.com/jxstxn1/hash_demo)\n\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the following:\n\n\n\nThis can be very frustrating especially if you start whitelisting scripts to your Content Security Policy and those scripts are requesting resources from somewhere else.\n\n**So what's the solution for this?**\n\nContent Security Policy errors can only be solved by the web server which is hosting the Web App. There are probably multiple solutions to tackle this problem, but today we are g"])</script>
385<script>self.__next_f.push([1,"oing to take a deep dive into `dart_frog` and how to utilize it to host our Flutter Web App with some advanced capabilities.\n\n### Key Topics\n\n\u003e Note: Content Security Policy will be called CSP in this Blog Post\n\n* Serve a Flutter Web App with `dart_frog`\n \n* Set all recommended security headers with `shelf_helmet`\n \n* Create a Content Security Policy that works with CSP2 and 3\n \n* Create Hashes for `inline-scripts` to whitelist them in our Content Security Policy\n \n\n### Setting everything up\n\nWe'll start by creating a brand new dart\\_frog project\n\n```bash\ndart_frog create hash_demo\n```\n\nWe can then open our `pubpsec.yaml` and replace the contents with\n\n```yaml\nname: hash_demo\ndescription: An example of how to use the CSP Hasher package in combination with shelf_helmet.\nversion: 1.0.0+1\npublish_to: none\n\nenvironment:\n sdk: \"\u003e=3.0.0 \u003c4.0.0\"\n\ndependencies:\n csp_hasher: ^1.0.0\n dart_frog: ^1.0.0\n path: ^1.8.3\n shelf_helmet: ^2.1.1\n\ndev_dependencies:\n mocktail: ^0.3.0\n test: ^1.19.2\n very_good_analysis: ^5.0.0\n```\n\nNow we have to create our Flutter project with: \n`flutter create counter --platform web`\n\n### Build and copy the Flutter App\n\nTo build our Flutter App we run in `counter`:\n\n```bash\nflutter build web --web-renderer canvaskit --release --csp\n```\n\nAfter the build, we can copy the output from `counter/build/web` to `public/` \nTo simplify this you can run from `hash_demo`:\n\n```bash\ncp -r counter/build/web/ public/\n```\n\n### Serve Flutter App at /\n\nTo serve the `index.html` file directly at the `/` location we modify the `routes/index.dart` file with:\n\n```dart\nimport 'dart:io';\n\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:path/path.dart' as path;\n\nResponse onRequest(RequestContext context) {\n final file = File(\n path.join(Directory.current.path, 'public', 'index.html'),\n );\n final indexHtml = file.readAsStringSync();\n return Response(body: indexHtml, headers: {'Content-Type': 'text/html'});\n}\n```\n\nTo ensure that everything works as expected we will write a test for it. \nLet's create `test/routes/index_test.dart` :\n\n```dart\nimport 'dart:io';\n\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\nimport '../../routes/index.dart' as route;\n\nclass _MockRequestContext extends Mock implements RequestContext {}\n\nfinal htmlString = File(\n '${Directory.current.path}/public/index.html',\n).readAsStringSync();\n\nvoid main() {\n group('GET /', () {\n test('responds with a 200, an html and the content_type header text/html',\n () async {\n final context = _MockRequestContext();\n final response = route.onRequest(context);\n expect(response.statusCode, equals(200));\n expect(\n response.headers,\n equals({\n 'Content-Type': 'text/html',\n 'content-length': '1830',\n }),\n );\n expect(response.body(), completion(htmlString));\n });\n });\n}\n```\n\nPerfect! We can now serve our Flutter Web App at `localhost:8080/` \nTo test it in a browser you can run:\n\n```bash\ndart_frog dev\n```\n\n### Creating our middleware\n\nWe can now create our middleware where most of the magic is happening. \nTo do so run:\n\n```bash\ndart_frog new middleware /\n```\n\nWe can now replace the content in the newly created middleware with this:\n\n```dart\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:shelf_helmet/shelf_helmet.dart';\n\nHandler middleware(Handler handler) {\n return handler.use(requestLogger()).use(\n fromShelfMiddleware(\n helmet(\n options: const HelmetOptions(\n cspOptions: ContentSecurityPolicyOptions.useDefaults(\n directives: {\n 'script-src': [\n \"'strict-dynamic'\",\n \"'wasm-unsafe-eval'\",\n \"'self'\",\n 'blob:',\n 'https://unpkg.com/',\n 'https://www.gstatic.
385com/flutter-canvaskit/',\n ],\n 'script-src-elem': [\n \"'self'\",\n 'blob:',\n "])</script>
385<script>self.__next_f.push([1," 'https://unpkg.com/',\n 'https://www.gstatic.com/flutter-canvaskit/',\n ],\n 'connect-src': [\n \"'self'\",\n 'https://unpkg.com/',\n 'https://www.gstatic.com/flutter-canvaskit/',\n 'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ',\n ],\n 'style-src': [\n \"'self'\",\n 'https:',\n ],\n 'require-trusted-types-for': [\"'script'\"],\n },\n ),\n ),\n ),\n ),\n );\n}\n```\n\nWe use `shelf_helmet` to set nearly all security headers. Since we are using a Flutter Web App we have to define some Urls in our CSP. This CSP is working with CSP3 and all previous versions.\n\n\u003e [You can find a deeper explanation of CSP here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)\n\u003e \n\u003e [And the documentation of shelf\\_helmet](https://pub.dev/packages/shelf_helmet)\n\nWe ensure that all recommended headers are set by creating a test for our middleware. So let's create `test/routes/_middleware_test.dart`:\n\n```dart\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\nimport '../../routes/_middleware.dart';\n\nclass _MockRequestContext extends Mock implements RequestContext {}\n\nvoid main() {\n group('Middleware', () {\n test('add all required headers', () async {\n final handler = middleware((context) =\u003e Response());\n final request = Request.get(Uri.parse('http://localhost/'));\n final context = _MockRequestContext();\n\n when(() =\u003e context.request).thenReturn(request);\n\n final finishedHandler = await handler(context);\n\n const cspRules =\n '''script-src 'strict-dynamic' 'wasm-unsafe-eval' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https:;require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests''';\n _expectedHeaders(finishedHandler.headers, cspRules);\n });\n });\n}\n\nvoid _expectedHeaders(Map\u003cString, String\u003e headers, String cspRules) {\n expect(headers['content-length'], '0');\n expect(headers['x-xss-protection'], '0');\n expect(headers['x-permitted-cross-domain-policies'], 'none');\n expect(headers['x-frame-options'], 'SAMEORIGIN');\n expect(headers['x-download-options'], 'noopen');\n expect(headers['x-dns-prefetch-control'], 'off');\n expect(headers['x-content-type-options'], 'nosniff');\n expect(\n headers['strict-transport-security'],\n 'max-age=15552000; includeSubDomains',\n );\n expect(headers['referrer-policy'], 'no-referrer');\n expect(headers['origin-agent-cluster'], '?1');\n expect(headers['cross-origin-resource-policy'], 'same-origin');\n expect(headers['cross-origin-opener-policy'], 'same-origin');\n expect(headers['Content-Security-Policy'], cspRules);\n}\n```\n\nIn our `_expectedHeaders` we can see the work of `shelf_helmet`.\n\n### Create hashes for `inline-scripts` with the `csp_hasher` package\n\nSince Flutter is using `inline-scripts` like this:\n\n```xml\n\u003cscript\u003e\n // The value below is injected by flutter build, do not touch.\n var serviceWorkerVersion = \"1515245720\";\n\u003c/script\u003e\n```\n\nwhich is causing our Content Security Policy to throw errors and cause our web app to not load we have to generate Hashes for those functions.\n\nLet's start by hashing our `inline-scripts` in a custom entrypoint at our project root\n\n```dart\n// custom entrypoint for the app\nimport 'dart:io';\n\nimport 'package:csp_hasher/csp_hasher.dart';\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:"])</script>
385<script>self.__next_f.push([1,"path/path.dart' as path;\n\nList\u003cCspHash\u003e cspScriptHashes = [];\nList\u003cCspHash\u003e cspStyleHashes = [];\n\nFuture\u003cHttpServer\u003e run(Handler handler, InternetAddress ip, int port) {\n generateCspHashes();\n return serve(handler, ip, port, poweredByHeader: null);\n}\n\n/// Generates CSP hashes for the scripts and styles in the index.html file\nvoid generateCspHashes() {\n final file = File(\n path.join(Directory.current.path, 'public', '-.html'),\n );\n if (!file.existsSync()) {\n throw Exception('Index Not found\\nPlease run build_web.sh first');\n }\n\n cspScriptHashes = hashScripts(htmlFile: file);\n cspStyleHashes = hashScripts(\n htmlFile: file,\n hashMode: HashMode.style,\n );\n}\n```\n\n\u003e [You can find a link to the `csp_hasher` package here](https://pub.dev/packages/csp_hasher)\n\nThis is creating hashes for all our `inline-scripts` at every hot-reload.\n\nWe can now inject the hashes into our Content Security Policy in the `_middleware.dart` file so that it looks like this:\n\n```dart\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:shelf_helmet/shelf_helmet.dart';\n\nimport '../main.dart';\n\nHandler middleware(Handler handler) {\n return handler.use(requestLogger()).use(\n fromShelfMiddleware(\n helmet(\n options: HelmetOptions(\n cspOptions: ContentSecurityPolicyOptions.useDefaults(\n directives: {\n 'script-src': [\n \"'strict-dynamic'\",\n \"'wasm-unsafe-eval'\",\n cspScriptHashes.join(' ').replaceAll('\"', ''),\n \"'self'\",\n 'blob:',\n 'https://unpkg.com/',\n 'https://www.gstatic.com/flutter-canvaskit/',\n ],\n 'script-src-elem': [\n cspScriptHashes.join(' ').replaceAll('\"', ''),\n \"'self'\",\n 'blob:',\n 'https://unpkg.com/',\n 'https://www.gstatic.com/flutter-canvaskit/',\n ],\n 'connect-src': [\n \"'self'\",\n 'https://unpkg.com/',\n 'https://www.gstatic.com/flutter-canvaskit/',\n 'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ',\n ],\n 'style-src': [\n \"'self'\",\n 'https:',\n cspStyleHashes.join(' ').replaceAll('\"', ''),\n ],\n 'require-trusted-types-for': [\"'script'\"],\n },\n ),\n ),\n ),\n ),\n );\n}\n```\n\nAnd our updated test is looking like this:\n\n```dart\nimport 'package:csp_hasher/csp_hasher.dart';\nimport 'package:dart_frog/dart_frog.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:test/test.dart';\n\nimport '../../main.dart';\nimport '../../routes/_middleware.dart';\n\nclass _MockRequestContext extends Mock implements RequestContext {}\n\nvoid main() {\n group('Middleware', () {\n setUp(() {\n cspScriptHashes.addAll([\n CspHash(\n lineNumber: 1,\n hashType: sha256,\n hash: 'abcdef',\n hashMode: HashMode.script,\n ),\n ]);\n cspStyleHashes.addAll([\n CspHash(\n lineNumber: 1,\n hashType: sha256,\n hash: 'ghijkl',\n hashMode: HashMode.style,\n ),\n ]);\n });\n test('add all required headers', () async {\n final handler = middleware((context) =\u003e Response());\n final request = Request.get(Uri.parse('http://localhost/'));\n final context = _MockRequestContext();\n\n when(() =\u003e context.request).thenReturn(request);\n\n final finishedHandler = await handler(context);\n\n const cspRules =\n '''script-src 'unsafe-inline' 'strict-dynamic' 'wasm-unsafe-eval' 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.
385com/flutter-canvaskit/;script-src-elem 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg."])</script>
385<script>self.__next_f.push([1,"com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https: 'sha256-ghijkl';require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests''';\n _expectedHeaders(finishedHandler.headers, cspRules);\n });\n });\n}\n\nvoid _expectedHeaders(Map\u003cString, String\u003e headers, String cspRules) {\n expect(headers['content-length'], '0');\n expect(headers['x-xss-protection'], '0');\n expect(headers['x-permitted-cross-domain-policies'], 'none');\n expect(headers['x-frame-options'], 'SAMEORIGIN');\n expect(headers['x-download-options'], 'noopen');\n expect(headers['x-dns-prefetch-control'], 'off');\n expect(headers['x-content-type-options'], 'nosniff');\n expect(\n headers['strict-transport-security'],\n 'max-age=15552000; includeSubDomains',\n );\n expect(headers['referrer-policy'], 'no-referrer');\n expect(headers['origin-agent-cluster'], '?1');\n expect(headers['cross-origin-resource-policy'], 'same-origin');\n expect(headers['cross-origin-opener-policy'], 'same-origin');\n expect(headers['Content-Security-Policy'], cspRules);\n}\n```\n\n### Conclusion\n\nBy utilizing `dart_frog` in combination with `shelf_helmet` and `csp_hasher` it is quite easy to serve a Flutter Web App with the recommended Security Headers and a very strong `Content-Security-Policy`. Nevertheless, we can avoid all the hashing by removing all `inline-scripts` from our `index.html`\n\n[Link to the GitHub Repo](https://github.com/jxstxn1/hash_demo)b:[\"$\",\"main\",null,{\"id\":\"main-content\",\"className\":\"min-h-screen bg-transparent\",\"children\":[[\"$\",\"$L17\",null,{\"publication\":{\"__typename\":\"Publication\",\"id\":\"63d295d13bb03008dbb2afb3\",\"url\":\"https://jxstxn.dev\",\"canonicalURL\":\"https://jxstxn.dev\",\"urlPattern\":\"simple\",\"title\":\"jxstxn\",\"displayTitle\":null,\"hasBadges\":true,\"descriptionSEO\":\"Hey, I'm jxstxn or Justin. I'm a Flutter Developer who loves Flutter and Dart. In my free time, I'm writing Dart Packages like Middlewares for Shelf or Plugins \",\"seo\":{\"title\":null,\"description\":null,\"__typename\":\"SEO\"},\"publicMembers\":{\"totalDocuments\":1,\"__typename\":\"PublicMembers\"},\"about\":{\"html\":\"\u003cp\u003eHey, I'm jxstxn or Justin. I'm a Flutter Developer who loves Flutter and Dart. In my free time, I'm writing Dart Packages like Middlewares for Shelf or Plugins for Sidekick.\u003c/p\u003e\\n\",\"text\":\"Hey, I'm jxstxn or Justin. I'm a Flutter Developer who loves Flutter and Dart. In my free time, I'm writing Dart Packages like Middlewares for Shelf or Plugins for Sidekick.\\n\",\"__typename\":\"Content\"},\"features\":{\"proTeam\":{\"isEnabled\":false,\"__typename\":\"ProTeamFeature\"},\"newsletter\":{\"isEnabled\":true,\"__typename\":\"NewsletterFeature\"},\"viewCount\":{\"isEnabled\":false,\"__typename\":\"ViewCountFeature\"},\"readTime\":{\"isEnabled\":true,\"__typename\":\"ReadTimeFeature\"},\"textSelectionSharer\":{\"isEnabled\":true,\"__typename\":\"TextSelectionSharerFeature\"},\"gptBotCrawling\":{\"__typename\":\"GPTBotCrawlingFeature\",\"isEnabled\":true},\"headlessCMS\":{\"isEnabled\":false,\"__typename\":\"HeadlessCMSFeature\"},\"__typename\":\"PublicationFeatures\"},\"aeoSettings\":{\"isEnabled\":false,\"llmsTxtEnabled\":true,\"crawlers\":{\"gptBot\":true,\"claudeBot\":true,\"perplexityBot\":true,\"googleExtended\":true,\"ccBot\":true,\"__typename\":\"AeoCrawlerSettings\"},\"__typename\":\"PublicationAeoSettings\"},\"metaTags\":null,\"ogMetaData\":{\"image\":null,\"__typename\":\"OpenGraphMetaData\"},\"author\":{\"__typename\":\"User\",\"id\":\"638f2499895a9daa034fdb2c\",\"name\":\"Justin Baumann\",\"username\":\"jxstxn1\",\"profilePicture\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1670325403203/m8iKNQWRdC.jpeg\"},\"preferences\":{\"__typename\":\"PublicationPreferences\",\"logo\":null,\"dar
385kMode\":{\"__typename\":\"DarkModePreferences\",\"logo\":null,\"enabled\":false},\"navbarItems\":[],\"enabledPages\":{\"__typename\":\"EnabledPages\",\"badges\":true,\"newsletter\":true,\"members\":true},\"layout\":\"stacked\",\"disableFooterBranding\":false,\"isSubscriptionModalDisabled\":false},\"favicon"])</script>
385<script>self.__next_f.push([1,"\":null,\"headerColor\":\"#92a4d3\",\"integrations\":{\"fbPixelID\":null,\"fathomSiteID\":null,\"fathomCustomDomainEnabled\":false,\"fathomCustomDomain\":null,\"hotjarSiteID\":null,\"matomoSiteID\":null,\"matomoURL\":null,\"gaTrackingID\":null,\"gTagManagerID\":null,\"plausibleAnalyticsEnabled\":false,\"wmPaymentPointer\":null,\"koalaPublicKey\":null,\"msClarityID\":null,\"openPanelClientId\":null,\"consentManagement\":{\"enabled\":false,\"version\":1,\"privacyPolicyURL\":null,\"__typename\":\"ConsentManagement\"},\"__typename\":\"PublicationIntegrations\"},\"imprintV2\":null,\"postsCount\":{\"totalDocuments\":1,\"__typename\":\"PostConnection\"},\"isTeam\":true,\"links\":{\"twitter\":\"https://twitter.com/jxstxn_____\",\"instagram\":\"\",\"github\":\"https://github.com/jxstxn1\",\"website\":\"https://jxstxn.dev/\",\"hashnode\":\"https://hashnode.com/@jxstxn1\",\"youtube\":\"\",\"dailydev\":\"\",\"linkedin\":\"https://www.linkedin.com/in/justin-baumann-flutter/\",\"mastodon\":\"https://fluttercommunity.social/@justin\",\"facebook\":null,\"bluesky\":null,\"__typename\":\"PublicationLinks\"},\"domainInfo\":{\"__typename\":\"DomainInfo\",\"hashnodeSubdomain\":\"jxstxn1\",\"domain\":{\"__typename\":\"Domain\",\"host\":\"jxstxn.dev\",\"ready\":true},\"wwwPrefixedDomain\":{\"__typename\":\"Domain\",\"host\":\"www.jxstxn.dev\",\"ready\":true}},\"redirectionRules\":[],\"customRules\":[],\"totalRecommendedPublications\":0,\"allowContributorEdits\":false,\"widgets\":[],\"post\":{\"__typename\":\"Post\",\"id\":\"64d5f9d6e6424c168da70ac5\",\"cuid\":\"cll6d4pkj001909mjhnhqa0bm\",\"title\":\"Elevate Your Flutter Web Hosting with Dart_Frog\",\"subtitle\":\"(and some more packages)\",\"slug\":\"elevate-your-flutter-web-hosting-with-dartfrog\",\"brief\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\",\"featured\":false,\"publishedAt\":\"2023-08-11T09:05:26.899Z\",\"updatedAt\":null,\"url\":\"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog\",\"readTimeInMinutes\":6,\"views\":465,\"reactionCount\":0,\"responseCount\":1,\"replyCount\":1,\"author\":{\"__typename\":\"User\",\"id\":\"638f2499895a9daa034fdb2c\",\"name\":\"Justin Baumann\",\"username\":\"jxstxn1\",\"deactivated\":false,\"profilePicture\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1670325403203/m8iKNQWRdC.jpeg\",\"bio\":{\"html\":\"\u003cp\u003eHey, I'm jxstxn or Justin. I'm a Flutter Developer who loves Flutter and Dart. In my free time, I'm writing Dart Packages like Middlewares for Shelf or Plugins for Sidekick.\u003c/p\u003e\\n\",\"__typename\":\"Content\"},\"socialMediaLinks\":{\"website\":\"https://linktr.ee/jxstxn\",\"github\":\"https://github.com/jxstxn1\",\"twitter\":\"https://twitter.com/jxstxn_____\",\"facebook\":\"\",\"stackoverflow\":\"\",\"linkedin\":\"https://www.linkedin.com/in/justin-baumann-flutter/\",\"__typename\":\"SocialMediaLinks\"}},\"coAuthors\":[],\"seo\":{\"title\":\"Elevate Your Flutter Web Hosting with Dart_Frog\",\"description\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\",\"__typename\":\"SEO\"},\"coverImage\":{\"url\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png\",\"isPortrait\":false,\"attribution\":\"\",\"isAttributionHidden\":false,\"photographer\":\"\",\"__typename\":\"CoverImage\"},\"content\":{\"html\":\"$18\",\"markdown\":\"$19\",\"__typename\":\"Content\"},\"preferences\":{\"pinnedToBlog\":false,\"disableComments\":false,\"stickCoverToBottom\":false,\"isDelisted\":false,\"__typename\":\"PostPreferences\"},\"series\":null,\"tags\":[{\"id\":\"56744721958ef13879b94df0\",\"slug\":\"dart\",\"name\":\"Dart\",\"__typename\":\"Tag\"},{\"id\":\"56744722958ef13879b9507c\",\"slug\":\"flutter\",\"name\":\"Flutter\",\"__typename\":\"Tag\"},{\"id\":\"62c4771d5527bf5d12a25bf9\",\"slug\":\"dart-language\",\"name\":\"#dart language\",\"__typename\":\"Tag\"},{\"id\":\"63a3dffcd1e499a9b8dfaa76\",\"slug\":\"dartfrog\",\"name\":\"dart_frog\",\"__typename\":\"Tag\"},{\"id\":\"64d5f9d64068aa09907925b1\",\"slug\":\"shelf\",\"name\":\"shelf\",\"__typename\":\"Tag\"}],\"ogMetaData\":{\"image\""])</script>
385<script>self.__next_f.push([1,":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691744613789/9a5f451a-bf52-459e-8b99-c91e5e592469.png\",\"__typename\":\"OpenGraphMetaData\"},\"canonicalUrl\":null,\"hasLatexInPost\":false,\"faq\":[],\"features\":{\"tableOfContents\":{\"isEnabled\":true,\"items\":[{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-0\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null},{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-1\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null},{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-2\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null},{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-3\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null},{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-4\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null},{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-5\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null},{\"__typename\":\"TableOfContentsItem\",\"id\":\"toc-6\",\"level\":1,\"slug\":\"\",\"title\":\"\",\"parentId\":null}],\"__typename\":\"TableOfContents\"},\"__typename\":\"PostFeatures\"}},\"posts\":{\"edges\":[{\"node\":{\"__typename\":\"Post\",\"id\":\"64d5f9d6e6424c168da70ac5\",\"title\":\"Elevate Your Flutter Web Hosting with Dart_Frog\",\"slug\":\"elevate-your-flutter-web-hosting-with-dartfrog\",\"publishedAt\":\"2023-08-11T09:05:26.899Z\",\"cuid\":\"cll6d4pkj001909mjhnhqa0bm\",\"url\":\"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog\",\"subtitle\":\"(and some more packages)\",\"brief\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\",\"readTimeInMinutes\":6,\"views\":465,\"tags\":[{\"id\":\"56744721958ef13879b94df0\",\"slug\":\"dart\",\"name\":\"Dart\",\"__typename\":\"Tag\"},{\"id\":\"56744722958ef13879b9507c\",\"slug\":\"flutter\",\"name\":\"Flutter\",\"__typename\":\"Tag\"},{\"id\":\"62c4771d5527bf5d12a25bf9\",\"slug\":\"dart-language\",\"name\":\"#dart language\",\"__typename\":\"Tag\"},{\"id\":\"63a3dffcd1e499a9b8dfaa76\",\"slug\":\"dartfrog\",\"name\":\"dart_frog\",\"__typename\":\"Tag\"},{\"id\":\"64d5f9d64068aa09907925b1\",\"slug\":\"shelf\",\"name\":\"shelf\",\"__typename\":\"Tag\"}],\"author\":{\"__typename\":\"User\",\"id\":\"638f2499895a9daa034fdb2c\",\"username\":\"jxstxn1\",\"name\":\"Justin Baumann\",\"profilePicture\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1670325403203/m8iKNQWRdC.jpeg\",\"followersCount\":3},\"coverImage\":{\"__typename\":\"CoverImage\",\"url\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png\",\"isPortrait\":false,\"isAttributionHidden\":false}},\"__typename\":\"PostEdge\"}],\"__typename\":\"PostConnection\"},\"redirectedPost\":null,\"staticPage\":null},\"posts\":[\"$b:props:children:0:props:publication:post\"],\"host\":\"jxstxn.dev\"}],\"$L1a\",\"$L1b\",null,false,\"$L1c\",null,\"$L1d\"]}]\n1e:I[62031,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"ReadingModeToggle\"]\n1f:I[81356,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"Image\"]\n20:I[52619,[\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.
385js\",\"2423\",\"static/"])</script>
385<script>self.__next_f.push([1,"chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"8974\",\"static/chunks/app/page-cc18ae1a3a7a5ed8.js\"],\"\"]\n21:I[33884,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"Avatar\"]\n22:I[33884,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"AvatarImage\"]\n23:I[33884,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"AvatarFallback\"]\n24:I[78655,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"TableOfContents\"]\n27:I[51285,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"CommentsSection\"]\n1a:[\"$\",\"section\",null,{\"className\":\"py-8\",\"children\":[\"$\",\"div\",null,{\"className\":\"px-4 sm:px-8\",\"children\":[\"$\",\"div\",null,{\"className\":\"w-full max-w-full xl:mx-auto xl:max-w-[80rem] 2xl:max-w-[90rem]\",\"children\":[\"$\",\"div\",null,{\"className\":\"grid gap-5 max-w-full\",\"children\":[\"$\",\"article\",null,{\"className\":\"w-full max-w-full rounded-xl border bg-card text-card-foreground\",\"children\":[[\"$\",\"div\",null,{\"className\":\"px-5 pt-12 pb-8 sm:px-8\",\"children\":[\"$\",\"header\",null,{\"className\":\"space-y-5\",\"children\":[\"$\",\"div\",null,{\"className\":\"space-y-4 text-center max-w-4xl mx-auto\",\"children\":[[\"$\",\"h1\",null,{\"className\":\"text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold leading-tight tracking-tight text-balance text-foreground\",\"children\":\"Elevate Your Flutter Web Hosting with Dart_Frog\"}],[\"$\",\"p\",null,{\"className\":\"text-lg sm:text-xl leading-normal font-normal text-muted-foreground text-balance\",\"children\":\"(and some more packages)"])</script>
385<script>self.__next_f.push([1,"\"}],[\"$\",\"div\",null,{\"className\":\"flex flex-wrap items-center justify-center gap-3 text-sm text-muted-foreground\",\"children\":[[\"$\",\"div\",null,{\"className\":\"flex items-center gap-2\",\"children\":[[\"$\",\"span\",null,{\"children\":\"Published\"}],[\"$\",\"time\",null,{\"dateTime\":\"2023-08-11T09:05:26.899Z\",\"children\":\"August 11, 2023\"}]]}],[[\"$\",\"span\",null,{\"className\":\"text-muted-foreground\",\"children\":\"â¢\"}],[\"$\",\"span\",null,{\"children\":[6,\" min read\"]}]],[\"$\",\"span\",null,{\"className\":\"text-muted-foreground\",\"children\":\"â¢\"}],[\"$\",\"$L1e\",null,{\"slug\":\"elevate-your-flutter-web-hosting-with-dartfrog\"}]]}]]}]}]}],[\"$\",\"div\",null,{\"className\":\"w-full\",\"children\":[\"$\",\"$L1f\",null,{\"src\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png\",\"alt\":\"Elevate Your Flutter Web Hosting with Dart_Frog\",\"width\":1200,\"height\":750,\"className\":\"w-full h-auto\",\"priority\":true}]}],[\"$\",\"div\",null,{\"className\":\"px-5 py-12 sm:px-8 space-y-8\",\"children\":[[\"$\",\"div\",null,{\"className\":\"max-w-2xl mx-auto\",\"children\":[\"$\",\"div\",null,{\"className\":\"flex flex-wrap items-center gap-5\",\"children\":[[\"$\",\"div\",\"638f2499895a9daa034fdb2c\",{\"className\":\"flex items-center gap-5\",\"children\":[false,[\"$\",\"div\",null,{\"className\":\"flex gap-3 items-start\",\"children\":[[\"$\",\"$L20\",null,{\"href\":\"https://hashnode.com/@jxstxn1\",\"target\":\"_blank\",\"rel\":\"noopener\",\"children\":[\"$\",\"$L21\",null,{\"className\":\"h-10 w-10 border border-border transition-opacity hover:opacity-80\",\"children\":[[\"$\",\"$L22\",null,{\"src\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1670325403203/m8iKNQWRdC.jpeg?auto=compress,format\u0026format=webp\",\"alt\":\"Justin Baumann\"}],[\"$\",\"$L23\",null,{\"className\":\"bg-muted text-muted-foreground font-semibold text-lg\",\"children\":\"J\"}]]}]}],[\"$\",\"div\",null,{\"className\":\"min-w-0\",\"children\":[[\"$\",\"div\",null,{\"className\":\"flex items-center gap-2\",\"children\":[[\"$\",\"$L20\",null,{\"href\":\"https://hashnode.com/@jxstxn1\",\"target\":\"_blank\",\"rel\":\"noopener\",\"className\":\"font-medium text-foreground hover:text-primary transition-colors\",\"children\":\"Justin Baumann\"}],[\"$\",\"div\",null,{\"className\":\"flex items-center gap-1.5 text-sm\",\"children\":[[\"$\",\"$L20\",null,{\"href\":\"https://twitter.com/jxstxn_____\",\"target\":\"_blank\",\"rel\":\"noopener\",\"className\":\"text-muted-foreground hover:text-foreground transition-colors\",\"aria-label\":\"X (Twitter)\",\"children\":[\"$\",\"i\",null,{\"className\":\"fa-brands fa-x-twitter\",\"aria-hidden\":true}]}],[\"$\",\"$L20\",null,{\"href\":\"https://www.linkedin.com/in/justin-baumann-flutter/\",\"target\":\"_blank\",\"rel\":\"noopener\",\"className\":\"text-muted-foreground hover:text-foreground transition-colors\",\"aria-label\":\"LinkedIn\",\"children\":[\"$\",\"i\",null,{\"className\":\"fa-brands fa-linkedin\",\"aria-hidden\":true}]}]]}]]}],[\"$\",\"div\",null,{\"className\":\"text-sm text-muted-foreground line-clamp-2\",\"dangerouslySetInnerHTML\":{\"__html\":\"\u003cp\u003eHey, I'm jxstxn or Justin. I'm a Flutter Developer who loves Flutter and Dart. In my free time, I'm writing Dart Packages like Middlewares for Shelf or Plugins for Sidekick.\u003c/p\u003e\\n\"}}]]}]]}]]}]]}]}],null,[\"$\",\"div\",null,{\"className\":\"relative\",\"children\":[[\"$\",\"aside\",null,{\"className\":\"hidden xl:block absolute top-0 left-0 h-full\",\"children\":[\"$\",\"nav\",null,{\"className\":\"sticky top-22 w-44\",\"children\":[\"$\",\"$L24\",null,{\"headings\":[{\"id\":\"heading-key-topics\",\"text\":\"Key Topics\",\"level\":3},{\"id\":\"heading-setting-everything-up\",\"text\":\"Setting everything up\",\"level\":3},{\"id\":\"heading-build-and-copy-the-flutter-app\",\"text\":\"Build and copy the Flutter App\",\"level\":3},{\"id\":\"heading-serve-flutter-app-at\",\"text\":\"Serve Flutter App at /\",\"level\":3},{\"id\":\"heading-creating-our-middleware\",\"text\":\"Creating our middleware\",\"level\":3},{\"id\":\"heading-create-hashes-for-inline-scripts-with-the-csphasher-package\",\"text\":\"Create hashes for inline-scripts with the csp_hasher package\",\"level\":3},{\"id\":\"heading-conclusion\",\"text\":\"Conclusion\",\"level\":3}]}]}]}],\"$L25\"]}],false,\"$L26\"]}]]}]}]}]}]}]\n1b:[\"$\",\"section\",null,{\"className\":\"pb-8\",\"children\":[\"$\",\"div\",null,{\"className\":\"px-4 sm:px-8\",\"children\":[\"$\",\"div\",null,{\"c"])</script>
385<script>self.__next_f.push([1,"lassName\":\"w-full max-w-full xl:mx-auto xl:max-w-[80rem] 2xl:max-w-[90rem]\",\"children\":[\"$\",\"div\",null,{\"className\":\"max-w-4xl w-full mx-auto\",\"children\":[\"$\",\"div\",null,{\"className\":\"rounded-xl border bg-card text-card-foreground p-5 sm:p-8\",\"children\":[\"$\",\"$L27\",null,{\"host\":\"jxstxn.dev\",\"slug\":\"elevate-your-flutter-web-hosting-with-dartfrog\",\"postUrl\":\"https://hashnode.com/posts/elevate-your-flutter-web-hosting-with-dartfrog/64d5f9d6e6424c168da70ac5\",\"initialEdges\":[{\"cursor\":\"1\",\"node\":{\"id\":\"64d606af5b9d5d38a49da5b2\",\"content\":{\"html\":\"\u003cp\u003eVery Informative!\u003c/p\u003e\\n\",\"__typename\":\"Content\"},\"dateAdded\":\"2023-08-11T10:00:15.638Z\",\"totalReactions\":0,\"author\":{\"id\":\"63fee1a6deb81e346f60788e\",\"name\":\"Dhairya Kubadia\",\"username\":\"kdhair14\",\"profilePicture\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1677648297108/176f7606-0f03-45cf-892c-41eadf5594c8.png\",\"__typename\":\"User\"},\"replies\":{\"edges\":[],\"pageInfo\":{\"hasNextPage\":false,\"endCursor\":\"50\",\"totalDocuments\":0,\"__typename\":\"PageInfo\"},\"__typename\":\"CommentConnection\"},\"__typename\":\"Comment\"},\"__typename\":\"CommentEdge\"}],\"initialPageInfo\":{\"hasNextPage\":false,\"endCursor\":\"10\",\"totalDocuments\":1,\"__typename\":\"PageInfo\"},\"totalComments\":1}]}]}]}]}]}]\n28:T470,{\"@context\":\"https://schema.org\",\"@type\":\"Article\",\"url\":\"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog\",\"mainEntityOfPage\":\"https://jxstxn.dev/elevate-your-flutter-web-hosting-with-dartfrog\",\"headline\":\"Elevate Your Flutter Web Hosting with Dart_Frog\",\"description\":\"Link to the GitHub Repo\\nEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the ...\",\"author\":{\"@type\":\"Person\",\"name\":\"Justin Baumann\",\"url\":\"https://hashnode.com/@jxstxn1\",\"image\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1670325403203/m8iKNQWRdC.jpeg\",\"sameAs\":\"https://x.com/jxstxn_____\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"jxstxn\",\"url\":\"https://jxstxn.dev\"},\"datePublished\":\"2023-08-11T09:05:26.899Z\",\"dateModified\":\"2023-08-11T09:05:26.899Z\",\"image\":{\"@type\":\"ImageObject\",\"url\":\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691681627081/86e3c434-e4bc-48d2-be27-7bf74c2eea36.png\"},\"keywords\":\"Dart, Flutter, #dart language, dart_frog, shelf\"}1c:[\"$\",\"script\",null,{\"type\":\"application/ld+json\",\"dangerouslySetInnerHTML\":{\"__html\":\"$28\"}}]\n29:T68e3,\u003cp\u003e\u003ca target=\"_blank\" href=\"https://github.com/jxstxn1/hash_demo\" rel=\"noopener ugc\"\u003eLink to the GitHub Repo\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eEveryone who worked with Flutter Web in a production environment knows this Problem. After a ten-minute deployment, you open the web app and you see a white screen. Then you start to investigate and your Browser tells you the following:\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://cdn.hashnode.com/res/hashnode/image/upload/v1691525897533/5137f6e7-1424-4949-bdf9-352276200e3e.png\" alt class=\"image--center mx-auto\" /\u003e\u003c/p\u003e\n\u003cp\u003eThis can be very frustrating especially if you start whitelisting scripts to your Content Security Policy and those scripts are requesting resources from somewhere else.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSo what's the solution for this?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eContent Security Policy errors can only be solved by the web server which is hosting the Web App. There are probably multiple solutions to tackle this problem, but today we are going to take a deep dive into \u003ccode\u003edart_frog\u003c/code\u003e and how to utilize it to host our Flutter Web App with some advanced capabilities.\u003c/p\u003e\n\u003ch3 id=\"heading-key-topics\"\u003eKey Topics\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eNote: Content Security Policy will be called CSP in this Blog Post\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cp\u003eServe a Flutter Web App with \u003ccode\u003edart_frog\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eSet all recommended security headers with \u003ccode\u003eshelf_helmet\u003c/code\u003e\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eCreate a Content Security Policy that works with CSP2 and 3\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cp\u003eCreate Hashes for \u003ccode\u003einline-scripts\u003c/code\u003e to whitelist them in our Content Security Policy\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"heading-setting-everything-up\"\u003eSett"])</script>
385<script>self.__next_f.push([1,"ing everything up\u003c/h3\u003e\n\u003cp\u003eWe'll start by creating a brand new dart_frog project\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003edart_frog create hash_demo\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWe can then open our \u003ccode\u003epubpsec.yaml\u003c/code\u003e and replace the contents with\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003e\u003cspan class=\"hljs-attr\"\u003ename:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ehash_demo\u003c/span\u003e\n\u003cspan class=\"hljs-attr\"\u003edescription:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eAn\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eexample\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eof\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ehow\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eto\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003euse\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ethe\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eCSP\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eHasher\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003epackage\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ein\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ecombination\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003ewith\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003eshelf_helmet.\u003c/span\u003e\n\u003cspan class=\"hljs-attr\"\u003eversion:\u003c/span\u003e \u003cspan class=\"hljs-number\"\u003e1.0\u003c/span\u003e\u003cspan class=\"hljs-number\"\u003e.0\u003c/span\u003e\u003cspan class=\"hljs-string\"\u003e+1\u003c/span\u003e\n\u003cspan class=\"hljs-attr\"\u003epublish_to:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003enone\u003c/span\u003e\n\n\u003cspan class=\"hljs-attr\"\u003eenvironment:\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003esdk:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e\"\u0026gt;=3.0.0 \u0026lt;4.0.0\"\u003c/span\u003e\n\n\u003cspan class=\"hljs-attr\"\u003edependencies:\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003ecsp_hasher:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.0.0\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003edart_frog:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.0.0\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003epath:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.8.3\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003eshelf_helmet:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^2.1.1\u003c/span\u003e\n\n\u003cspan class=\"hljs-attr\"\u003edev_dependencies:\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003emocktail:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^0.3.0\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003etest:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^1.19.2\u003c/span\u003e\n \u003cspan class=\"hljs-attr\"\u003every_good_analysis:\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e^5.0.0\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eNow we have to create our Flutter project with:\u003cbr /\u003e\u003ccode\u003eflutter create counter --platform web\u003c/code\u003e\u003c/p\u003e\n\u003ch3 id=\"heading-build-and-copy-the-flutter-app\"\u003eBuild and copy the Flutter App\u003c/h3\u003e\n\u003cp\u003eTo build our Flutter App we run in \u003ccode\u003ecounter\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003eflutter build web --web-renderer canvaskit --release --csp\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAfter the build, we can copy the output from \u003ccode\u003ecounter/build/web\u003c/code\u003e to \u003ccode\u003epublic/\u003c/code\u003e\u003cbr /\u003eTo simplify this you can run from \u003ccode\u003ehash_demo\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003ecp -r counter/build/web/ public/\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"heading-serve-flutter-app-at\"\u003eServe Flutter App at /\u003c/h3\u003e\n\u003cp\u003eTo serve the \u003ccode\u003eindex.html\u003c/code\u003e file directly at the \u003ccode\u003e/\u003c/code\u003e location we modify the \u
385003ccode\u003eroutes/index.dart\u003c/code\u003e file with:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'dart:io'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:path/path.dart'\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eas\u003c/span\u003e path;\n\nResponse onRequest(RequestContext context) {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e file = File(\n path.join(Directory.current.path, \u003cspan class=\"hljs-string\"\u003e'public'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e'index.html'\u003c/span\u003e),\n );\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e indexHtml = file.readAsStringSync();\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e Response(body: indexHtml, headers: {\u003cspan class=\"hljs-string\"\u003e'Content-Type'\u003c/span\u003e: \u003cspan class=\"hljs-string\"\u003e'text/html'\u003c/span\u003e});\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eTo ensure that everything works as expected we will write a test for it.\u003cbr /\u003eLet's create \u003ccode\u003etest/routes/index_test.dart\u003c/code\u003e :\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'dart:io'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/s"])</script>
385<script>self.__next_f.push([1,"pan\u003e \u003cspan class=\"hljs-string\"\u003e'package:mocktail/mocktail.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:test/test.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../routes/index.dart'\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eas\u003c/span\u003e route;\n\n\u003cspan class=\"hljs-class\"\u003e\u003cspan class=\"hljs-keyword\"\u003eclass\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003e_MockRequestContext\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eextends\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eMock\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eimplements\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eRequestContext\u003c/span\u003e \u003c/span\u003e{}\n\n\u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e htmlString = File(\n \u003cspan class=\"hljs-string\"\u003e'\u003cspan class=\"hljs-subst\"\u003e${Directory.current.path}\u003c/span\u003e/public/index.html'\u003c/span\u003e,\n).readAsStringSync();\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e main() {\n group(\u003cspan class=\"hljs-string\"\u003e'GET /'\u003c/span\u003e, () {\n test(\u003cspan class=\"hljs-string\"\u003e'responds with a 200, an html and the content_type header text/html'\u003c/span\u003e,\n () \u003cspan class=\"hljs-keyword\"\u003easync\u003c/span\u003e {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e context = _MockRequestContext();\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e response = route.onRequest(context);\n expect(response.statusCode, equals(\u003cspan class=\"hljs-number\"\u003e200\u003c/span\u003e));\n expect(\n response.headers,\n equals({\n \u003cspan class=\"hljs-string\"\u003e'Content-Type'\u003c/span\u003e: \u003cspan class=\"hljs-string\"\u003e'text/html'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'content-length'\u003c/span\u003e: \u003cspan class=\"hljs-string\"\u003e'1830'\u003c/span\u003e,\n }),\n );\n expect(response.body(), completion(htmlString));\n });\n });\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ePerfect! We can now serve our Flutter Web App at \u003ccode\u003elocalhost:8080/\u003c/code\u003e\u003cbr /\u003eTo test it in a browser you can run:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003edart_frog dev\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"heading-creating-our-middleware\"\u003eCreating our middleware\u003c/h3\u003e\n\u003cp\u003eWe can now create our middleware where most of the magic is happening.\u003cbr /\u003eTo do so run:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-bash\"\u003edart_frog new middleware /\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWe can now replace the content in the newly created middleware with this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:shelf_helmet/shelf_helmet.dart'\u003c/span\u003e;\n\nHandler middleware(Handler handler) {\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e handler.use(requestLogger()).use(\n fromShelfMiddleware(\n helmet(\n options: \u003cspan class=\"hljs-keyword\"\u003econst\u003c/span\u003e HelmetOptions(\n cspOptions: ContentSecurityPolicyOptions.useDefaults(\n directives: {\n \u003cspan class=\"hljs-string\"\u003e'script-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'strict-dynamic'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e\"'wasm-unsafe-eval'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.
385com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'script-src-elem'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'connect-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://fonts.gstatic.com/s/ro"])</script>
385<script>self.__next_f.push([1,"boto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf '\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'style-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https:'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'require-trusted-types-for'\u003c/span\u003e: [\u003cspan class=\"hljs-string\"\u003e\"'script'\"\u003c/span\u003e],\n },\n ),\n ),\n ),\n ),\n );\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eWe use \u003ccode\u003eshelf_helmet\u003c/code\u003e to set nearly all security headers. Since we are using a Flutter Web App we have to define some Urls in our CSP. This CSP is working with CSP3 and all previous versions.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP\" rel=\"noopener ugc\"\u003eYou can find a deeper explanation of CSP here\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://pub.dev/packages/shelf_helmet\" rel=\"noopener ugc\"\u003eAnd the documentation of shelf_helmet\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWe ensure that all recommended headers are set by creating a test for our middleware. So let's create \u003ccode\u003etest/routes/_middleware_test.dart\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:mocktail/mocktail.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:test/test.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../routes/_middleware.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-class\"\u003e\u003cspan class=\"hljs-keyword\"\u003eclass\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003e_MockRequestContext\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eextends\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eMock\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eimplements\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eRequestContext\u003c/span\u003e \u003c/span\u003e{}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e main() {\n group(\u003cspan class=\"hljs-string\"\u003e'Middleware'\u003c/span\u003e, () {\n test(\u003cspan class=\"hljs-string\"\u003e'add all required headers'\u003c/span\u003e, () \u003cspan class=\"hljs-keyword\"\u003easync\u003c/span\u003e {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e handler = middleware((context) =\u0026gt; Response());\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e request = Request.\u003cspan class=\"hljs-keyword\"\u003eget\u003c/span\u003e(\u003cspan class=\"hljs-built_in\"\u003eUri\u003c/span\u003e.parse(\u003cspan class=\"hljs-string\"\u003e'http://localhost/'\u003c/span\u003e));\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e context = _MockRequestContext();\n\n when(() =\u0026gt; context.request).thenReturn(request);\n\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e finishedHandler = \u003cspan class=\"hljs-keyword\"\u003eawait\u003c/span\u003e handler(context);\n\n \u003cspan class=\"hljs-keyword\"\u003econst\u003c/span\u003e cspRules =\n \u003cspan class=\"hljs-string\"\u003e'''script-src 'strict-dynamic' 'wasm-unsafe-eval' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;
385connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https:;require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests'''\u003c/span\u003e;\n _expectedHeaders(finishedHandler.headers, cspRules);\n });\n });\n}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e _expectedHeaders(\u003cspan class=\"hljs-built_in\"\u003eMap\u003c/span\u003e\u0026lt;\u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e\u0026gt; headers, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e cspRules) {\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'content-length'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-xss-protection'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-permitted-cross-domain-policies'\u003c/span\u003e], \u003cspan cl"])</script>
385<script>self.__next_f.push([1,"ass=\"hljs-string\"\u003e'none'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-frame-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'SAMEORIGIN'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-download-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'noopen'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-dns-prefetch-control'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'off'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-content-type-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'nosniff'\u003c/span\u003e);\n expect(\n headers[\u003cspan class=\"hljs-string\"\u003e'strict-transport-security'\u003c/span\u003e],\n \u003cspan class=\"hljs-string\"\u003e'max-age=15552000; includeSubDomains'\u003c/span\u003e,\n );\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'referrer-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'no-referrer'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'origin-agent-cluster'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'?1'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-resource-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-opener-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'Content-Security-Policy'\u003c/span\u003e], cspRules);\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIn our \u003ccode\u003e_expectedHeaders\u003c/code\u003e we can see the work of \u003ccode\u003eshelf_helmet\u003c/code\u003e.\u003c/p\u003e\n\u003ch3 id=\"heading-create-hashes-for-inline-scripts-with-the-csphasher-package\"\u003eCreate hashes for \u003ccode\u003einline-scripts\u003c/code\u003e with the \u003ccode\u003ecsp_hasher\u003c/code\u003e package\u003c/h3\u003e\n\u003cp\u003eSince Flutter is using \u003ccode\u003einline-scripts\u003c/code\u003e like this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-xml\"\u003e\u003cspan class=\"hljs-tag\"\u003e\u0026lt;\u003cspan class=\"hljs-name\"\u003escript\u003c/span\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"javascript\"\u003e\n \u003cspan class=\"hljs-comment\"\u003e// The value below is injected by flutter build, do not touch.\u003c/span\u003e\n \u003cspan class=\"hljs-keyword\"\u003evar\u003c/span\u003e serviceWorkerVersion = \u003cspan class=\"hljs-string\"\u003e\"1515245720\"\u003c/span\u003e;\n\u003c/span\u003e\u003cspan class=\"hljs-tag\"\u003e\u0026lt;/\u003cspan class=\"hljs-name\"\u003escript\u003c/span\u003e\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003ewhich is causing our Content Security Policy to throw errors and cause our web app to not load we have to generate Hashes for those functions.\u003c/p\u003e\n\u003cp\u003eLet's start by hashing our \u003ccode\u003einline-scripts\u003c/code\u003e in a custom entrypoint at our project root\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-comment\"\u003e// custom entrypoint for the app\u003c/span\u003e\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'dart:io'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:csp_hasher/csp_hasher.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:path/path.dart'\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eas\u003c/span\u003e path;\n\n\u003cspan class=\"hljs-built_in\"\u003eList\u003c/span\u003e\u0026lt;CspHash\u0026gt; cspScriptHashes = [];\n\u003cspan class=\"hljs-built_in\"\u003eList\u003c/span\u003e\u0026lt;CspHash\u0026gt; cspStyleHashes = [];\n\nFuture\u0026lt;HttpServer\u0026gt; run(Handler handler, InternetAddress ip, \u003cspan class=\"hljs-built_in\"\u003eint\u003c/span\u003e port) {\n generateCspHashes();\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e serve(handler, ip, port, poweredByHeader: \u003cspan class=\"hljs-keyword\"\u003enull\u003c/span\u003e);\n}\n\n\u003cspan class=\"hljs-comment\"\u003e/// \u003cspan class=\"markdown\"\u003eGenerates CSP hashes for the scripts and styles in the index.html file\u003c/span\u003e\u003c/span\u003e\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e generateCspHashes() {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e file = File(\n path.join(Directory.current.path, \u003cspan class=\"hljs-string\"\u003e'public'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e'-.html'\u003c/span\u003e),\n );\n \u003cspan class=\"hljs-keyword\"\u003eif\u003c/span\u003e (!file.existsSync()) {\n \u003cspan class=\"hljs-keyword\"\u003ethrow\u003c/span\u003e Exception(\u003cspan class=\"hljs-string\"\u003e'Index Not found\\
385nPlease run build_web.sh first'\u003c/span\u003e);\n }\n\n cspScriptHashes = hashScripts(htmlFile: file);\n cspStyleHashes = hashScripts(\n htmlFile: file,\n hashMode: HashMode.style,\n );\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://pub.dev/packages/csp_hasher\" rel=\"no"])</script>
385<script>self.__next_f.push([1,"opener ugc\"\u003eYou can find a link to the \u003ccode\u003ecsp_hasher\u003c/code\u003e package here\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis is creating hashes for all our \u003ccode\u003einline-scripts\u003c/code\u003e at every hot-reload.\u003c/p\u003e\n\u003cp\u003eWe can now inject the hashes into our Content Security Policy in the \u003ccode\u003e_middleware.dart\u003c/code\u003e file so that it looks like this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:shelf_helmet/shelf_helmet.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../main.dart'\u003c/span\u003e;\n\nHandler middleware(Handler handler) {\n \u003cspan class=\"hljs-keyword\"\u003ereturn\u003c/span\u003e handler.use(requestLogger()).use(\n fromShelfMiddleware(\n helmet(\n options: HelmetOptions(\n cspOptions: ContentSecurityPolicyOptions.useDefaults(\n directives: {\n \u003cspan class=\"hljs-string\"\u003e'script-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'strict-dynamic'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e\"'wasm-unsafe-eval'\"\u003c/span\u003e,\n cspScriptHashes.join(\u003cspan class=\"hljs-string\"\u003e' '\u003c/span\u003e).replaceAll(\u003cspan class=\"hljs-string\"\u003e'\"'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e''\u003c/span\u003e),\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'script-src-elem'\u003c/span\u003e: [\n cspScriptHashes.join(\u003cspan class=\"hljs-string\"\u003e' '\u003c/span\u003e).replaceAll(\u003cspan class=\"hljs-string\"\u003e'\"'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e''\u003c/span\u003e),\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'blob:'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'connect-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://unpkg.com/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://www.gstatic.com/flutter-canvaskit/'\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf '\u003c/span\u003e,\n ],\n \u003cspan class=\"hljs-string\"\u003e'style-src'\u003c/span\u003e: [\n \u003cspan class=\"hljs-string\"\u003e\"'self'\"\u003c/span\u003e,\n \u003cspan class=\"hljs-string\"\u003e'https:'\u003c/span\u003e,\n cspStyleHashes.join(\u003cspan class=\"hljs-string\"\u003e' '\u003c/span\u003e).replaceAll(\u003cspan class=\"hljs-string\"\u003e'\"'\u003c/span\u003e, \u003cspan class=\"hljs-string\"\u003e''\u003c/span\u003e),\n ],\n \u003cspan class=\"hljs-string\"\u003e'require-trusted-types-for'\u003c/span\u003e: [\u003cspan class=\"hljs-string\"\u003e\"'script'\"\u003c/span\u003e],\n },\n ),\n ),\n ),\n ),\n );\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eAnd our updated test is looking like this:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-dart\"\u003e\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:csp_hasher/csp_hasher.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:dart_frog/dart_frog.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:mocktail/mocktail.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'package:test/test.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../main.dart'\u003c/span\u003e;\n\u003cspan class=\"hljs-keyword\"\u003eimport\u003c/span\u003e \u003cspan class=\"hljs-string\"\u003e'../../routes/_middleware.dart'\u003c/span\u003e;\n\n\u003cspan class=\"hljs-class\"\u003e\u003cspan class=\"hljs-keyword\"\u003eclass\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003e_MockR"])</script>
385<script>self.__next_f.push([1,"equestContext\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eextends\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eMock\u003c/span\u003e \u003cspan class=\"hljs-keyword\"\u003eimplements\u003c/span\u003e \u003cspan class=\"hljs-title\"\u003eRequestContext\u003c/span\u003e \u003c/span\u003e{}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e main() {\n group(\u003cspan class=\"hljs-string\"\u003e'Middleware'\u003c/span\u003e, () {\n setUp(() {\n cspScriptHashes.addAll([\n CspHash(\n lineNumber: \u003cspan class=\"hljs-number\"\u003e1\u003c/span\u003e,\n hashType: sha256,\n hash: \u003cspan class=\"hljs-string\"\u003e'abcdef'\u003c/span\u003e,\n hashMode: HashMode.script,\n ),\n ]);\n cspStyleHashes.addAll([\n CspHash(\n lineNumber: \u003cspan class=\"hljs-number\"\u003e1\u003c/span\u003e,\n hashType: sha256,\n hash: \u003cspan class=\"hljs-string\"\u003e'ghijkl'\u003c/span\u003e,\n hashMode: HashMode.style,\n ),\n ]);\n });\n test(\u003cspan class=\"hljs-string\"\u003e'add all required headers'\u003c/span\u003e, () \u003cspan class=\"hljs-keyword\"\u003easync\u003c/span\u003e {\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e handler = middleware((context) =\u0026gt; Response());\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e request = Request.\u003cspan class=\"hljs-keyword\"\u003eget\u003c/span\u003e(\u003cspan class=\"hljs-built_in\"\u003eUri\u003c/span\u003e.parse(\u003cspan class=\"hljs-string\"\u003e'http://localhost/'\u003c/span\u003e));\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e context = _MockRequestContext();\n\n when(() =\u0026gt; context.request).thenReturn(request);\n\n \u003cspan class=\"hljs-keyword\"\u003efinal\u003c/span\u003e finishedHandler = \u003cspan class=\"hljs-keyword\"\u003eawait\u003c/span\u003e handler(context);\n\n \u003cspan class=\"hljs-keyword\"\u003econst\u003c/span\u003e cspRules =\n \u003cspan class=\"hljs-string\"\u003e'''script-src 'unsafe-inline' 'strict-dynamic' 'wasm-unsafe-eval' 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;script-src-elem 'sha256-abcdef' 'self' blob: https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/;connect-src 'self' https://unpkg.com/ https://www.gstatic.com/flutter-canvaskit/ https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf ;style-src 'self' https: 'sha256-ghijkl';require-trusted-types-for 'script';default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src-attr 'none';upgrade-insecure-requests'''\u003c/span\u003e;\n _expectedHeaders(finishedHandler.headers, cspRules);\n });\n });\n}\n\n\u003cspan class=\"hljs-keyword\"\u003evoid\u003c/span\u003e _expectedHeaders(\u003cspan class=\"hljs-built_in\"\u003eMap\u003c/span\u003e\u0026lt;\u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e\u0026gt; headers, \u003cspan class=\"hljs-built_in\"\u003eString\u003c/span\u003e cspRules) {\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'content-length'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-xss-protection'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'0'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-permitted-cross-domain-policies'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'none'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-frame-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'SAMEORIGIN'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-download-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'noopen'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-dns-prefetch-control'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'off'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'x-content-type-options'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'nosniff'\u003c/span\u003e);\n expect(\n headers[\u003cspan class=\"hljs-string\"\u003e'strict-transport-security'\u003c/span\u003e],\n \u003cspan class=\"hljs-string\"\u003e'max-age=15552000; includeSubDomains'\u003c/span\u003e,\n );\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'referrer-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'no-referrer'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'origin-agent-cluster'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'?1'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-resource-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-origin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'cross-origin-opener-policy'\u003c/span\u003e], \u003cspan class=\"hljs-string\"\u003e'same-ori"])</script>
385<script>self.__next_f.push([1,"gin'\u003c/span\u003e);\n expect(headers[\u003cspan class=\"hljs-string\"\u003e'Content-Security-Policy'\u003c/span\u003e], cspRules);\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch3 id=\"heading-conclusion\"\u003eConclusion\u003c/h3\u003e\n\u003cp\u003eBy utilizing \u003ccode\u003edart_frog\u003c/code\u003e in combination with \u003ccode\u003eshelf_helmet\u003c/code\u003e and \u003ccode\u003ecsp_hasher\u003c/code\u003e it is quite easy to serve a Flutter Web App with the recommended Security Headers and a very strong \u003ccode\u003eContent-Security-Policy\u003c/code\u003e. Nevertheless, we can avoid all the hashing by removing all \u003ccode\u003einline-scripts\u003c/code\u003e from our \u003ccode\u003eindex.html\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca target=\"_blank\" href=\"https://github.com/jxstxn1/hash_demo\" rel=\"noopener ugc\"\u003eLink to the GitHub Repo\u003c/a\u003e\u003c/p\u003e\n25:[\"$\",\"div\",null,{\"className\":\"max-w-2xl mx-auto min-w-0 contain-[inline-size]\",\"children\":[\"$\",\"div\",null,{\"className\":\"prose dark:prose-invert prose-pre:bg-foreground prose-pre:text-background prose-pre:border prose-pre:border-border prose-pre:rounded prose-pre:p
385x-5 prose-pre:py-4 dark:prose-pre:bg-muted dark:prose-pre:text-foreground dark:prose-pre:border-border prose-pre:overflow-x-auto max-w-none text-base sm:text-lg [\u0026\u003ediv\u003ep:first-child]:mt-0 [\u0026\u003ediv\u003ep:first-child]:pt-0 min-w-0 wrap-break-word [\u0026_a]:break-all **:max-w-full\",\"children\":[[],[\"$\",\"div\",null,{\"suppressHydrationWarning\":true,\"dangerouslySetInnerHTML\":{\"__html\":\"$29\"}}],[],\"$L2a\",\"$L2b\",\"$L2c\",\"$L2d\",\"$L2e\"]}]}]\n26:[\"$\",\"div\",null,{\"className\":\"max-w-2xl mx-auto border-t border-border pt-8 space-y-5\",\"children\":[[\"$\",\"div\",null,{\"className\":\"flex flex-wrap items-center gap-2\",\"children\":[[\"$\",\"$L20\",\"56744721958ef13879b94df0\",{\"href\":\"/tag/dart\",\"className\":\"inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary\",\"children\":[\"#\",\"dart\"]}],[\"$\",\"$L20\",\"56744722958ef13879b9507c\",{\"href\":\"/tag/flutter\",\"className\":\"inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary\",\"children\":[\"#\",\"flutter\"]}],[\"$\",\"$L20\",\"62c4771d5527bf5d12a25bf9\",{\"href\":\"/tag/dart-language\",\"className\":\"inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary\",\"children\":[\"#\",\"dart-language\"]}],[\"$\",\"$L20\",\"63a3dffcd1e499a9b8dfaa76\",{\"href\":\"/tag/dartfrog\",\"className\":\"inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary\",\"children\":[\"#\",\"dartfrog\"]}],[\"$\",\"$L20\",\"64d5f9d64068aa09907925b1\",{\"href\":\"/tag/shelf\",\"className\":\"inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1 text-sm text-muted-foreground transition hover:border-primary hover:text-primary\",\"children\":[\"#\",\"shelf\"]}]]}],false]}]\n2f:I[32478,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"CodeHighlighter\"]\n30:I[7714,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"EmbedHandler\"]\n31:I[54960,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-98"])</script>
385<script>self.__next_f.push([1,"4b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"ImageZoom\"]\n32:I[6671,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"MathJaxHandler\"]\n33:I[91337,[\"590\",\"static/chunks/0dbeb660-9e82e99813552576.js\",\"4554\",\"static/chunks/4554-984b480a9db5f3fe.js\",\"7563\",\"static/chunks/7563-4dfa0994a9b0e37c.js\",\"5001\",\"static/chunks/5001-fae0bebdd93f95e3.js\",\"2423\",\"static/chunks/2423-c8b1388240338d7d.js\",\"9216\",\"static/chunks/9216-9272709ba40c1cfd.js\",\"1545\",\"static/chunks/1545-7abf96433ff6553f.js\",\"3091\",\"static/chunks/3091-bf1537ff6571a1bd.js\",\"652\",\"static/chunks/652-f40dcb480b4e356f.js\",\"730\",\"static/chunks/730-8191af9e9424b148.js\",\"3048\",\"static/chunks/app/%5B...slug%5D/page-ce41bf29cdfb993a.js\"],\"MermaidHandler\"]\n2a:[\"$\",\"$L2f\",null,{}]\n2b:[\"$\",\"$L30\",null,{}]\n2c:[\"$\",\"$L31\",null,{}]\n2d:[\"$\",\"$L32\",null,{\"hasLatex\":false}]\n2e:[\"$\",\"$L33\",null,{}]\n34:Tc98,M268.379 39q-6.322 0-10.35-4.108-4.002-4.134-4.002-10.483 0-6.296 4.002-10.43 4-4.135 10.163-4.135 6.028 0 9.817 3.734 3.814 3.735 3.814 9.497v2.8h-24.248v-4.054h17.633q-.187-2.747-2.054-4.508-1.868-1.76-4.962-1.76-3.28-.001-5.388 2.374-2.108 2.373-2.108 6.375 0 4.188 2.348 6.695 2.347 2.481 5.895 2.481 1.68 0 2.934-.453a8.5 8.5 0 0 0 2.321-1.307q1.041-.854 2.268-2.614l4.694 2.96q-1.707 2.748-3.494 4.135a12.7 12.7 0 0 1-3.975 2.081q-2.187.72-5.308.72m-33.496 0q-5.575 0-9.283-4.028-3.681-4.029-3.681-10.484 0-6.43 3.761-10.51 3.76-4.108 9.47-4.108 2.8 0 5.175 1.2 2.4 1.174 3.628 3.148l.026-.08V0h7.016v38.2h-6.722v-4.722l.64.8-.694.133q-1.28 2.108-3.788 3.361Q237.924 39 234.883 39m1.627-6.002q3.308 0 5.495-2.428t2.188-6.135q0-3.762-2.214-6.109-2.214-2.374-5.469-2.374-3.28 0-5.522 2.347-2.214 2.348-2.214 6.163 0 3.76 2.214 6.162 2.214 2.374 5.522 2.374M205.811 39q-6.376 0-10.537-4.161-4.161-4.188-4.161-10.404 0-6.321 4.161-10.456 4.188-4.135 10.537-4.135 6.348 0 10.537 4.161 4.188 4.161 4.188 10.43 0 6.216-4.188 10.404Q212.186 39 205.811 39m0-6.162q3.307 0 5.575-2.374 2.267-2.374 2.267-6.055 0-3.707-2.294-6.056-2.267-2.347-5.548-2.347-3.308 0-5.575 2.374-2.268 2.348-2.268 6.029t2.241 6.055q2.268 2.374 5.602 2.374M162.586 38.2V10.644h6.802v5.015l-.8-.987h.906q1.308-2.187 3.762-3.494 2.48-1.335 5.228-1.334 4.615 0 7.256 2.8 2.667 2.776 2.667 7.603V38.2h-7.069V21.794q0-2.854-1.387-4.401t-4.134-1.547q-2.614 0-4.402 1.974-1.76 1.974-1.76 4.881V38.2zm-30.181 0V0h6.962v14.698h.134q1.2-2.187 3.574-3.521 2.375-1.335 5.229-1.334 4.615 0 7.255 2.828 2.668 2.801 2.668 7.522V38.2h-7.069V21.794q0-2.827-1.387-4.402-1.387-1.573-4.055-1.573-2.587 0-4.428 2.027-1.814 2.027-1.814 4.962v15.391zm-14.416.8q-2.882 0-5.015-.64-2.108-.614-3.761-1.84-1.654-1.228-2.668-2.695l-.987-1.467 4.695-3.174.773 1.067a9.1 9.1 0 0 0 1.788 1.814 8.2 8.2 0 0 0 2.32 1.2q1.308.4 3.068.4 2.161 0 3.441-.8 1.308-.8 1.307-2.294 0-1.36-1.147-2.108-1.12-.747-5.068-1.654-5.335-1.253-7.656-3.334-2.32-2.107-2.321-5.362 0-3.76 3.148-6.002 3.148-2.267 7.923-2.267 2.454 0 4.348.507t3.388 1.493q1.493.988 2.454 2.188l.987 1.2-4.482 3.255-.667-.774a7.4 7.4 0 0 0-1.627-1.387 6.4 6.4 0 0 0-2.054-.88 9.5 9.5 0 0 0-2.401-.294q-1.974 0-3.201.8-1.2.774-1.2 1.921 0 1.254 1.12 2.027 1.147.748 5.229 1.681 5.334 1.227 7.629 3.308 2.294 2.055 2.294 5.3"])</script>
385<script>self.__next_f.push([1,"08 0 4.029-3.228 6.43Q123.217 39 117.989 39M87.61 39q-4.508 0-7.335-2.454-2.828-2.481-2.828-6.482 0-4.188 3.254-6.669t8.483-2.48q2.082 0 4.028.426 1.974.427 3.361 1.12v-1.6q0-2.588-1.813-4.082-1.788-1.493-4.615-1.493-1.628 0-2.801.373a8.7 8.7 0 0 0-2.188.987q-.987.614-1.84 1.414l-.854.8-4.081-3.788 1.307-1.173a18.7 18.7 0 0 1 2.934-2.108q1.601-.933 3.601-1.44t4.589-.507q5.868 0 9.229 2.88 3.361 2.882 3.361 8.084V38.2h-6.749v-5.922l1.654 2.187h-1.76q-1.308 2.055-3.655 3.308Q90.545 39 87.611 39m1.814-4.988q3.148 0 5.149-2.187 2-2.215 2-5.309v-.08q-1.12-.693-2.72-1.094a13 13 0 0 0-3.388-.426q-2.828 0-4.562 1.2-1.734 1.173-1.734 3.521 0 2.028 1.468 3.201t3.787 1.174M49 38.2V0h6.962v14.698h.134q1.2-2.187 3.574-3.521 2.375-1.335 5.229-1.334 4.615 0 7.255 2.828 2.668 2.801 2.668 7.522V38.2h-7.07V21.794q0-2.827-1.386-4.402-1.387-1.573-4.055-1.573-2.587 0-4.428 2.027-1.815 2.027-1.814 4.962v15.391z1d:[\"$\",\"footer\",null,{\"className\":\"px-4 pt-8 pb-20 sm:px-8 text-muted-foreground\",\"aria-label\":\"Footer\",\"children\":[\"$\",\"div\",null,{\"className\":\"w-full max-w-4xl mx-auto\",\"children\":[\"$\",\"div\",null,{\"className\":\"flex flex-col items-center gap-4 text-center text-sm\",\"children\":[false,[\"$\",\"p\",null,{\"className\":\"font-medium\",\"children\":[[\"$\",\"time\",null,{\"dateTime\":\"2026\",\"children\":[[\"$\",\"span\",null,{\"className\":\"font-sans\",\"children\":\"©\"}],\" \",2026]}],\" \",\"jxstxn\"]}],[\"$\",\"nav\",null,{\"aria-label\":\"Footer navigation\",\"children\":[\"$\",\"ul\",null,{\"className\":\"flex flex-wrap justify-center gap-4 text-sm tracking-wide\",\"children\":[[\"$\",\"li\",\"/members\",{\"children\":[\"$\",\"$L20\",null,{\"href\":\"/members\",\"className\":\"transition hover:text-foreground\",\"target\":\"$undefined\",\"rel\":\"$undefined\",\"children\":\"Members\"}]}],[\"$\",\"li\",\"/archive\",{\"children\":[\"$\",\"$L20\",null,{\"href\":\"/archive\",\"className\":\"transition hover:text-foreground\",\"target\":\"$undefined\",\"rel\":\"$undefined\",\"children\":\"Archive\"}]}],[\"$\",\"li\",\"https://hashnode.com/privacy-policy\",{\"children\":[\"$\",\"$L20\",null,{\"href\":\"https://hashnode.com/privacy-policy\",\"className\":\"transition hover:text-foreground\",\"target\":\"_blank\",\"rel\":\"noopener\",\"children\":\"Privacy\"}]}],[\"$\",\"li\",\"https://hashnode.com/terms\",{\"children\":[\"$\",\"$L20\",null,{\"href\":\"https://hashnode.com/terms\",\"className\":\"transition hover:text-foreground\",\"target\":\"_blank\",\"rel\":\"noopener\",\"children\":\"Terms\"}]}]]}]}],[\"$\",\"div\",null,{\"className\":\"pt-8 flex justify-center gap-6\",\"children\":[[\"$\",\"$L20\",null,{\"href\":\"/sitemap.xml\",\"target\":\"_blank\",\"rel\":\"noopener\",\"className\":\"flex items-center gap-2 text-sm transition hover:text-foreground\",\"title\":\"Sitemap\",\"children\":[[\"$\",\"i\",null,{\"className\":\"fa-regular fa-sitemap\"}],[\"$\",\"span\",null,{\"children\":\"Sitemap\"}]]}],[\"$\",\"$L20\",null,{\"href\":\"/rss.xml\",\"target\":\"_blank\",\"rel\":\"noopener\",\"className\":\"flex items-center gap-2 text-sm transition hover:text-foreground\",\"title\":\"RSS Feed\",\"children\":[[\"$\",\"i\",null,{\"className\":\"fa-regular fa-rss\"}],[\"$\",\"span\",null,{\"children\":\"RSS\"}]]}]]}],[\"$\",\"div\",null,{\"className\":\"pt-10 text-center\",\"children\":[\"$\",\"$L20\",null,{\"href\":\"https://hashnode.com/?utm_source=https%3A%2F%2Fjxstxn.dev\u0026utm_medium=referral\u0026utm_campaign=blog_footer_logo\u0026utm_content=logo\",\"target\":\"_blank\",\"rel\":\"noopener\",\"aria-label\":\"Visit Hashnode (opens in a new tab)\",\"children\":[\"$\",\"svg\",null,{\"fill\":\"none\",\"viewBox\":\"0 0 282 39\",\"aria-hidden\":\"true\",\"className\":\"h-6 w-auto min-w-[9.5rem] text-muted-foreground transition hover:text-foreground\",\"children\":[[\"$\",\"path\",null,{\"fill\":\"currentColor\",\"d\":\"M2.68 13.032c-3.573 3.505-3.573 9.363 0 12.936L13.032 36.32c3.505 3.573 9.363 3.573 12.936 0L36.32 25.968c3.573-3.573 3.573-9.431 0-12.936L25.968 2.68c-3.573-3.573-9.43
3851-3.573-12.936 0zm12.211 1.935c2.507-2.521 6.582-2.544 9.104-.038s2.544 6.582.038 9.104-6.582 2.544-9.104.038-2.544-6.582-.038-9.104\"}],[\"$\",\"path\",null,{\"fill\":\"currentColor\",\"d\":\"$34\"}]]}],\"className\":\"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-"])</script>
385<script>self.__next_f.push([1,"50 [\u0026_svg]:pointer-events-none [\u0026_svg]:size-4 [\u0026_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-10 rounded-md text-xl px-5 py-4\",\"ref\":null}]}]]}]}]}]\n"])</script>
385</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.