<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LayerCall</title>
    <description>The latest articles on DEV Community by LayerCall (layercall).</description>
    <link>https://dev.to/layercall</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14258%2F3f8d45d2-1077-4bdc-a203-9657d0c39468.png</url>
      <title>DEV Community: LayerCall</title>
      <link>https://dev.to/layercall</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/layercall"/>
    <language>en</language>
    <item>
      <title>Who is responsible when an agent does something on your behalf?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Wed, 26 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/who-is-responsible-when-an-agent-does-something-on-your-behalf-969</link>
      <guid>https://dev.to/layercall/who-is-responsible-when-an-agent-does-something-on-your-behalf-969</guid>
      <description>&lt;p&gt;This one is less technical and I think it matters more than the technical ones.&lt;/p&gt;

&lt;p&gt;A user instructs an agent. The agent takes an action on a third-party service. The action turns out to be wrong — the wrong plan bought, the wrong data shared, terms accepted that the person would not have accepted.&lt;/p&gt;

&lt;p&gt;Who is accountable? The user who gave a vague instruction? The agent operator whose model interpreted it? The service that accepted the request without checking there was a human behind it?&lt;/p&gt;

&lt;p&gt;Right now the honest answer is that nobody has decided, and every party has an incentive to point at the other two.&lt;/p&gt;

&lt;p&gt;What makes it more than a philosophical question is that it determines what we should be building. If the service is accountable, we need to be able to prove there was human intent behind a request — which is an engineering problem with real answers emerging. If the operator is accountable, they need auditable records of what was asked and what was done. If the user is accountable, then agents need to be far more explicit about what they are about to do than most currently are.&lt;/p&gt;

&lt;p&gt;I lean towards: the operator carries it, because they are the only party who can see both the instruction and the action. But that puts enormous weight on agent operators, and I am not sure they have signed up for it.&lt;/p&gt;

&lt;p&gt;Where do you think this lands? And is anyone building as though they already know?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>security</category>
      <category>career</category>
    </item>
    <item>
      <title>What does "verified" actually mean in your stack?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/what-does-verified-actually-mean-in-your-stack-56k8</link>
      <guid>https://dev.to/layercall/what-does-verified-actually-mean-in-your-stack-56k8</guid>
      <description>&lt;p&gt;I have started thinking the most dangerous word in a codebase is a field called &lt;code&gt;verified&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because it usually means one of two completely different things, and they get stored in the same column.&lt;/p&gt;

&lt;p&gt;Sometimes it means proven: a signature checked out, a cryptographic assertion held. That is arithmetic. It is either true or the maths failed.&lt;/p&gt;

&lt;p&gt;Sometimes it means inferred: heuristics agreed, the fingerprint looked normal, the score was low enough. That is judgement, and judgement a capable adversary patches in an afternoon.&lt;/p&gt;

&lt;p&gt;Once those share a field, somebody downstream branches on it — and they are now treating a guess exactly like a certainty. That is how a system starts refusing real customers on a hunch while its own code reads as though it knew something.&lt;/p&gt;

&lt;p&gt;What I do now is keep a single boolean that means only "this was settled cryptographically", and never let anything inferred touch it. Everything else lives in a score or a set of signals, where it is obviously an opinion. It costs an extra field and removes a whole category of mistake.&lt;/p&gt;

&lt;p&gt;The same applies to &lt;code&gt;is_bot&lt;/code&gt;, &lt;code&gt;is_fraud&lt;/code&gt;, &lt;code&gt;trusted&lt;/code&gt; — any name that sounds like a fact but holds an estimate.&lt;/p&gt;

&lt;p&gt;Does anyone have a naming convention that actually holds up over a few years and a few engineers? &lt;code&gt;_proven&lt;/code&gt; versus &lt;code&gt;_likely&lt;/code&gt; suffixes feel clumsy but at least they are honest.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>When did you last check who is actually calling your API?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Sat, 22 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/when-did-you-last-check-who-is-actually-calling-your-api-im1</link>
      <guid>https://dev.to/layercall/when-did-you-last-check-who-is-actually-calling-your-api-im1</guid>
      <description>&lt;p&gt;Here is a question worth being uncomfortable about: if one of your API keys leaked right now, how would you find out?&lt;/p&gt;

&lt;p&gt;Not "how would you rotate it" — that part is easy. How would you &lt;em&gt;notice&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;A stolen key does not look stolen. It authenticates correctly, because it is a valid key. It hits the same endpoints, because whoever took it wants the same functionality you built. It bills to the same account. Every row it writes to your usage log looks exactly like a row you wrote.&lt;/p&gt;

&lt;p&gt;The only thing that differs is where it comes from. Someone else's server is not the one you deploy from.&lt;/p&gt;

&lt;p&gt;Which means if you are not recording the caller's address alongside the usage, there is no evidence a theft ever happened. You find out from the bill, weeks later, and even then you cannot tell which calls were yours.&lt;/p&gt;

&lt;p&gt;What makes this a nasty class of problem is that the fix is trivial and almost nobody does it until after the incident. One column. The hard part is the alerting: most legitimate integrations call from several addresses — a few app instances, a laptop, CI — so "more than one address" fires constantly and gets ignored, and then it is not there on the day it matters.&lt;/p&gt;

&lt;p&gt;The heuristic I settled on: never flag the busiest source, and only flag a minority share once there is enough traffic for a share to mean anything. Quiet enough to still be believed.&lt;/p&gt;

&lt;p&gt;What do you do — per-key IP allowlists, anomaly alerting, or nothing until something looks weird on the invoice?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>The signal everyone blocks on is the one that is usually wrong</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Thu, 20 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/the-signal-everyone-blocks-on-is-the-one-that-is-usually-wrong-5be5</link>
      <guid>https://dev.to/layercall/the-signal-everyone-blocks-on-is-the-one-that-is-usually-wrong-5be5</guid>
      <description>&lt;p&gt;Datacenter IP is the first thing most teams block on, and on its own it is a terrible reason to refuse anyone.&lt;/p&gt;

&lt;p&gt;It tells you the request did not come from a phone or home broadband — it came from a rented server. That is genuinely useful information. It is just not a verdict, because the population it catches is mostly VPN users, and VPN users are disproportionately the security-conscious, privacy-aware, technically-literate people you would quite like as customers.&lt;/p&gt;

&lt;p&gt;Same for disposable email domains: often it is someone protecting their inbox from a product they have not decided to trust yet. Same for a freshly registered domain: sometimes it is a founder who launched last week and is signing up for tools.&lt;/p&gt;

&lt;p&gt;Each of those in isolation is a reason to look closer. None is a reason to refuse.&lt;/p&gt;

&lt;p&gt;Where it becomes unambiguous is combination. A disposable address, from a datacenter address, on a domain registered yesterday, as the fortieth signup from that subnet this hour — nobody is arguing that is a customer having a bad day.&lt;/p&gt;

&lt;p&gt;The general shape: single strong signals are how you refuse real people who never tell you. Combinations are how you catch the thing you were actually looking for. And blocking on any one field is almost always a decision someone made because it was the easiest thing to query, not because it was the most accurate.&lt;/p&gt;

&lt;p&gt;Which single signal does your stack block on that you suspect you should not?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>A risk score is not a probability. Does your code know that?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Tue, 18 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/a-risk-score-is-not-a-probability-does-your-code-know-that-401g</link>
      <guid>https://dev.to/layercall/a-risk-score-is-not-a-probability-does-your-code-know-that-401g</guid>
      <description>&lt;p&gt;A short one, but I keep seeing it cause real damage.&lt;/p&gt;

&lt;p&gt;A fraud or risk score of 80 means "riskier than 40". It does not mean an 80% chance of fraud. It is an ordering, not a probability — and most scoring systems, ours included, do not claim otherwise anywhere in their docs.&lt;/p&gt;

&lt;p&gt;Then someone builds arithmetic on top of it. Expected loss as &lt;code&gt;score/100 × transaction_value&lt;/code&gt;. Averaging scores across a session. Multiplying two scores together for a combined risk. Every one of those operations is meaningful on probabilities and meaningless on an ordering, and none of them throw an error. You get a number. The number is confident. It means nothing.&lt;/p&gt;

&lt;p&gt;The tell is any expression where you divide a score by 100, or where two scores get combined by anything other than max().&lt;/p&gt;

&lt;p&gt;What makes it hard to catch is that the resulting system often behaves &lt;em&gt;approximately&lt;/em&gt; right, because the ordering survives even when the arithmetic is nonsense. It fails at the edges, quietly, in the direction nobody is measuring.&lt;/p&gt;

&lt;p&gt;If you want a real probability you have to calibrate against outcomes you have actually observed — which means feeding results back and measuring, not deriving it from the score.&lt;/p&gt;

&lt;p&gt;Has anyone found a good way to stop this at review time? A lint rule feels too crude, but "remember scores are ordinal" in a doc nobody reads is clearly not working either.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Your API's redirect is eating the Authorization header</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Mon, 17 Aug 2026 02:03:06 +0000</pubDate>
      <link>https://dev.to/layercall/your-apis-redirect-is-eating-the-authorization-header-138i</link>
      <guid>https://dev.to/layercall/your-apis-redirect-is-eating-the-authorization-header-138i</guid>
      <description>&lt;p&gt;We shipped a bug last week where a valid API key arrived at our server as no key at all. The cause is a line in the fetch spec that most of us have read and none of us remember, and it will bite anyone whose API answers on both &lt;code&gt;example.com&lt;/code&gt; and &lt;code&gt;www.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the whole thing, as it was before we fixed it. Our apex no longer redirects, so running this against us today no longer reproduces it — swap in any API of your own that answers on both hosts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# apex, which 308'd to www&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com/v1/score/ip?ip=8.8.8.8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt;
&lt;span class="c"&gt;# {"error":"Missing API key..."}&lt;/span&gt;

&lt;span class="c"&gt;# www directly&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://www.example.com/v1/score/ip?ip=8.8.8.8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# {"error":"Invalid or revoked API key."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second error is the good one. "Invalid or revoked" means the key arrived and was judged. "Missing" means it never got there — and the only difference between the two calls is a redirect from the apex domain to &lt;code&gt;www&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; when a redirect changes host, clients strip &lt;code&gt;Authorization&lt;/code&gt;. It is in the fetch spec, it is in curl since 7.58, and it is in every browser. It is a good rule — following a redirect to an attacker-controlled host with your bearer token attached is how tokens leak — and it is invisible until it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes it nasty&lt;/strong&gt; is the shape of the failure. &lt;code&gt;X-Api-Key&lt;/code&gt; is a custom header and survives the hop. So our SDKs, which send &lt;code&gt;X-Api-Key&lt;/code&gt;, were fine. Only &lt;code&gt;Authorization: Bearer&lt;/code&gt; broke — which meant the people hitting it were the ones reading the docs and writing curl by hand, and the error they got said "missing API key" while they were looking straight at the key they had just pasted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it came back.&lt;/strong&gt; We fixed this in July. It regressed in August because the redirect is a setting in a hosting dashboard, not a line in the repo — so no code review could catch it and no test was watching. That is the part worth taking away: a bug whose cause lives outside your codebase will come back, and the only defence is a test that asserts the behaviour from outside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ the apex host does not redirect /v1 (a redirect strips the key)
    apex returned 308 -&amp;gt; https://www.example.com/v1/... ;
    a keyed request following this arrives unauthenticated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix, if you have this:&lt;/strong&gt; serve your API on whichever host was asked for, and redirect only the pages. Machine surfaces — your API, your JS snippet, your OpenAPI file, &lt;code&gt;.well-known&lt;/code&gt; — should not redirect at all. Canonicalise the HTML and leave the machines alone.&lt;/p&gt;

&lt;p&gt;That is what we did, and the assertion above is now green. A page on the apex still 308s to www, so there is still one canonical URL in the index; &lt;code&gt;/v1&lt;/code&gt; and the other machine paths answer on whichever host you ask.&lt;/p&gt;

&lt;p&gt;If you would rather not change your hosting: at minimum, say so in your 401. Ours now reads "check you called &lt;a href="https://www.layercall.com" rel="noopener noreferrer"&gt;https://www.layercall.com&lt;/a&gt;" — because the developer hitting this has no way to guess, and the header they lost is not visible from where they are standing.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>http</category>
      <category>debugging</category>
    </item>
    <item>
      <title>We treat a missing signature as suspicious. Should we?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Sun, 16 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/we-treat-a-missing-signature-as-suspicious-should-we-819</link>
      <guid>https://dev.to/layercall/we-treat-a-missing-signature-as-suspicious-should-we-819</guid>
      <description>&lt;p&gt;As request-signing standards land for automated traffic, a question is going to come up fast: what do you do with everything that is not signed?&lt;/p&gt;

&lt;p&gt;The tempting answer is to treat unsigned as untrusted, then gradually squeeze it. It feels like a migration path.&lt;/p&gt;

&lt;p&gt;I think it is a mistake, and the reason is a distinction worth naming: absence of a claim is not a false claim. An unsigned request has told you nothing. It has not lied. Most traffic on the internet is unsigned and most of it is completely fine — including, for a long while yet, most legitimate agents whose operators have not adopted anything.&lt;/p&gt;

&lt;p&gt;Where it gets genuinely difficult is that this is exactly how good standards die. Nobody adopts because there is no benefit; there is no benefit because nobody adopts. Signing only pays off if verifiers do something meaningfully better for signed traffic than unsigned.&lt;/p&gt;

&lt;p&gt;So the honest position seems to be: signed and verified earns you a faster path, higher limits, fewer challenges. Unsigned earns you the normal path, not a penalty. Reward the claim, do not punish its absence.&lt;/p&gt;

&lt;p&gt;But I hold that loosely, because "reward, never penalise" might just be too slow to bootstrap anything.&lt;/p&gt;

&lt;p&gt;Where do you land? Is there a version of penalising unsigned traffic that is not just breaking the web for everyone who has not caught up?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your false positives never complain. So how do you find them?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Fri, 14 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/your-false-positives-never-complain-so-how-do-you-find-them-111n</link>
      <guid>https://dev.to/layercall/your-false-positives-never-complain-so-how-do-you-find-them-111n</guid>
      <description>&lt;p&gt;Any system that blocks people has two failure modes, and only one of them is loud.&lt;/p&gt;

&lt;p&gt;Fraud that gets through arrives as a chargeback, a support ticket, a cleanup job. It has a cost you can put in a spreadsheet and a person whose week it ruined. It gets reviewed.&lt;/p&gt;

&lt;p&gt;A real customer wrongly refused closes the tab. They do not file a bug. They do not email support. Nothing in your dashboard records that they existed, and the metric that would have caught it is the absence of an event.&lt;/p&gt;

&lt;p&gt;The structural consequence is what bothers me: if the only feedback reaching you is the loud kind, then every review concludes the rules are too loose, and they tighten. Forever. Not because anyone decided to be aggressive, but because one side of the ledger is invisible and the other is not.&lt;/p&gt;

&lt;p&gt;Things I have seen actually work against it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Count challenges &lt;em&gt;issued&lt;/em&gt; against challenges &lt;em&gt;passed&lt;/em&gt;. People passing your step-up at a high rate means the band is doing its job; people abandoning it means it is set too wide.&lt;/li&gt;
&lt;li&gt;Run in log-only mode for a period and look at the score distribution before acting on it, so the cutoff comes from your traffic rather than someone else's.&lt;/li&gt;
&lt;li&gt;Sample blocked traffic and actually look at it, on a schedule, as a job someone owns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are clever. They just require someone to go looking for the thing that does not announce itself.&lt;/p&gt;

&lt;p&gt;What do you use? I am genuinely collecting approaches here.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>Should a customer's AI assistant be allowed to sign up for them?</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/should-a-customers-ai-assistant-be-allowed-to-sign-up-for-them-1aek</link>
      <guid>https://dev.to/layercall/should-a-customers-ai-assistant-be-allowed-to-sign-up-for-them-1aek</guid>
      <description>&lt;p&gt;Genuine question, and I do not think there is consensus yet.&lt;/p&gt;

&lt;p&gt;A user tells their assistant: find me a tool that does X, make an account, start the trial. The agent does exactly that. It fills in the form with the user's real details, receives the verification email in the user's real mailbox, clicks the link.&lt;/p&gt;

&lt;p&gt;Is that a signup you want?&lt;/p&gt;

&lt;p&gt;The case for yes: it is a real person who genuinely wants your product, and the only difference from a normal signup is which fingers typed. Refusing it means refusing a customer for the crime of using a tool. That reflex gets more expensive every month, and it is invisible in your metrics — refused signups do not complain.&lt;/p&gt;

&lt;p&gt;The case for no: your terms probably say a human must accept them. Nobody read your onboarding. Nobody saw the pricing page. The person may not even know which product got chosen. And whatever the agent agreed to on their behalf, you may struggle to enforce.&lt;/p&gt;

&lt;p&gt;My own position is that the signup is fine and the &lt;em&gt;terms acceptance&lt;/em&gt; is the actual unsolved bit — but I hold it loosely.&lt;/p&gt;

&lt;p&gt;There is also a version that is straightforwardly not fine: the same assistant, told to open fifty trial accounts. It will honestly declare itself user-initiated every single time, because it is telling the truth. Identity does not catch that; only volume does.&lt;/p&gt;

&lt;p&gt;What are you doing in your product right now — allowing, blocking, or has it not come up yet?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>webdev</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>My health check passed for weeks while reading nothing</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:45:44 +0000</pubDate>
      <link>https://dev.to/layercall/my-health-check-passed-for-weeks-while-reading-nothing-2c7f</link>
      <guid>https://dev.to/layercall/my-health-check-passed-for-weeks-while-reading-nothing-2c7f</guid>
      <description>&lt;p&gt;One of my monitoring checks reported "all clear" on every run for weeks. It was reading an empty list and calling that good news.&lt;/p&gt;

&lt;p&gt;Here's the whole bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The alarm: is any real customer on our email provider's suppression list?&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.provider.com/v3/suppressions?limit=500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contacts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;        &lt;span class="c1"&gt;// &amp;lt;-- the bug lives here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider caps &lt;code&gt;limit&lt;/code&gt; at 100. Ask for 500 and you get HTTP 400 with &lt;code&gt;{"code":"out_of_range"}&lt;/code&gt;. There's no &lt;code&gt;contacts&lt;/code&gt; key on an error body, so &lt;code&gt;?? []&lt;/code&gt; turned a failed request into an empty list, and the step went off and reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok | suppression list holds no real customer — 0 blocked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero blocked. Beautifully green. It had never successfully read the list in its life.&lt;/p&gt;

&lt;p&gt;The thing that gets me is that it would have gone green in exactly the same way if every single customer had been suppressed. The output had nothing to do with the thing it was watching, and no way to say so.&lt;/p&gt;

&lt;p&gt;That's worse than having no check at all, because the green tick is doing active harm — it's answering a question you now think you've covered.&lt;/p&gt;

&lt;p&gt;These are hard to spot. They never fail, so they never show up in an incident. They never flap, so they look like your most stable check. And the code reads fine — &lt;code&gt;?? []&lt;/code&gt; is idiomatic, it looks defensive, it's the kind of line you skim past approvingly. Reading the diff would not have caught this.&lt;/p&gt;

&lt;p&gt;What caught it was a second check disagreeing.&lt;/p&gt;

&lt;p&gt;I'd just added a different step that picks a genuinely suppressed address off the live list and asserts our login form refuses it with a reason. It ran in the same fifteen-minute cycle and found one. The older step, same run, said zero. One check saying "here's one" next to another saying "there are none" is what made it visible.&lt;/p&gt;

&lt;p&gt;The fix is two lines and slightly embarrassing. Cap the limit at 100 and paginate. And treat a failed read as a failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`provider would not return the list (HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An alarm that can't read its input has to say so. "I don't know" and "nothing is wrong" are different answers and only one of them is safe to paint green.&lt;/p&gt;

&lt;p&gt;The other thing I'd do differently: test the check against something you know is broken. Every one of these I've found had never been run against a true positive. If your alarm has never once fired on purpose, you don't actually know it can.&lt;/p&gt;

&lt;p&gt;I made that exact mistake twice in one day, incidentally. The replacement check I wrote first looked up a single address and treated HTTP 200 as suppressed — except that path 404s for suppressed and healthy addresses alike, because it only exists for DELETE. Would have reported "fine" for every customer, forever, without ever failing. I only found out because I pointed it at an address I already knew was on the list.&lt;/p&gt;

&lt;p&gt;If you want to go looking tonight, the shape to grep for is anywhere a failure can be read as an empty or zero result. &lt;code&gt;?? []&lt;/code&gt; on a parsed response. &lt;code&gt;data?.rows?.length ?? 0&lt;/code&gt; where 0 means healthy. A try/catch returning a default. Any count-based alert where "we got nothing back" and "there's nothing to report" produce the same number.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>The unsubscribe link you can't remove is also your login</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:43:46 +0000</pubDate>
      <link>https://dev.to/layercall/the-unsubscribe-link-you-cant-remove-is-also-your-login-3nhh</link>
      <guid>https://dev.to/layercall/the-unsubscribe-link-you-cant-remove-is-also-your-login-3nhh</guid>
      <description>&lt;p&gt;Someone can click "unsubscribe" on a sign-in code and lock themselves out of their own account. You won't find out. The send reports success, same as always.&lt;/p&gt;

&lt;p&gt;Here's how that happens.&lt;/p&gt;

&lt;p&gt;Our email provider is CSA-certified, which means it attaches a &lt;code&gt;List-Unsubscribe&lt;/code&gt; header to everything it relays. Not just marketing. Everything. There's no setting to turn it off for a transactional-only account, and when I set my own &lt;code&gt;List-Unsubscribe&lt;/code&gt; it got quietly replaced with theirs. I pulled the raw source of a delivered message to check, because I didn't believe the docs.&lt;/p&gt;

&lt;p&gt;Mail clients turn that header into a button. Apple Mail puts an Unsubscribe banner across the top of the message. Gmail does its own version.&lt;/p&gt;

&lt;p&gt;So the six-digit code you send someone to get into their account arrives with an unsubscribe button stapled to it.&lt;/p&gt;

&lt;p&gt;Click it and the address goes on the provider's suppression list. After that every message to that person is dropped at the relay. The API still returns 2xx. Nothing in your app notices. They ask for a code, your login form says "check your inbox", and nothing is ever going to arrive. They can't get in, and they can't tell you either, because your support flow emails them too.&lt;/p&gt;

&lt;p&gt;No error, no bounce, no log line. From your side everything looks fine. From theirs your product is just broken.&lt;/p&gt;

&lt;p&gt;The clean fix is a separate provider for auth mail only, and that's where I'm heading. It's not a quick job though. Your auth provider sends its own confirmations and magic links, so pointing that at a different relay means new credentials, new DNS, a warm-up period and a second sender reputation to look after. Until that's done your sign-in codes and your newsletters live or die together.&lt;/p&gt;

&lt;p&gt;So here's what I did in the meantime.&lt;/p&gt;

&lt;p&gt;First, check the suppression list before triggering the code. You might not be able to ask about one address — on mine, &lt;code&gt;GET /blockedContacts/{email}&lt;/code&gt; returns 404 whether or not the address is on the list, because that path only exists for &lt;code&gt;DELETE&lt;/code&gt;. The list endpoint ignores an email filter too. So you fetch the whole list and match locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cached, because this sits on the login path.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;suppressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ESP_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// fail open, always&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWholeSuppressionList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// paginate it&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;     &lt;span class="c1"&gt;// keep stale on failure&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// reason, or null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I got this wrong the first time in a way worth stealing. I checked for HTTP 200 on the per-address path, and since that path 404s for suppressed and healthy addresses alike, my check reported "fine" for every customer, forever, without ever failing. I only caught it because I tried it against an address I already knew was on the list. Do that.&lt;/p&gt;

&lt;p&gt;It also has to fail open. A login form that turns people away because a third-party API timed out is a worse outage than the one you're preventing.&lt;/p&gt;

&lt;p&gt;Second, say which reason it is. A hard bounce and an unsubscribe are opposite situations. Telling someone "you unsubscribed" when their mailbox actually rejected us is wrong and a bit insulting — their address is fine. So: unsubscribed gets "this address was unsubscribed from our messages, which also stops sign-in codes", and a bounce gets "earlier mail to this address bounced, so our provider stopped trying — if the address is right, ask us to re-enable it."&lt;/p&gt;

&lt;p&gt;Third, give them a way back that doesn't need email. This is the part people skip. A support form works while they're suppressed, because filing a ticket doesn't require you to email them. A support email address obviously doesn't. And say plainly on the confirmation screen that no confirmation email is coming, since that's the exact thing they're writing in about.&lt;/p&gt;

&lt;p&gt;One thing I'd argue against: don't automate the un-suppression.&lt;/p&gt;

&lt;p&gt;A hard bounce means the mailbox doesn't exist, and re-sending to it damages your sending reputation for every other customer. That's how a domain ends up filtered. An unsubscribe means they chose, and reversing that automatically is what GDPR and CAN-SPAM specifically prohibit — doing it systematically on a CSA-certified account puts the account itself at risk, and that account is carrying your sign-in codes. The blast radius of getting it wrong is much bigger than the problem.&lt;/p&gt;

&lt;p&gt;"A customer says they clicked it by accident" is real and common. That's a person's call. One click in your admin tool, not a cron job.&lt;/p&gt;

&lt;p&gt;The email specifics here are mine, but the shape isn't. Somewhere in your system is an action that reports success while doing nothing, and the only symptom is a customer who goes quiet.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>email</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>There is no such thing as a good fraud score threshold</title>
      <dc:creator>Hira</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/layercall/there-is-no-such-thing-as-a-good-fraud-score-threshold-2jgg</link>
      <guid>https://dev.to/layercall/there-is-no-such-thing-as-a-good-fraud-score-threshold-2jgg</guid>
      <description>&lt;p&gt;"What threshold should I set?" gets answered with a number far too often. Any number given without knowing the flow is someone guessing on your behalf.&lt;/p&gt;

&lt;p&gt;The score is identical everywhere in your product. What changes is what a mistake costs.&lt;/p&gt;

&lt;p&gt;Wrongly blocking a newsletter signup costs you an email address. Wrongly blocking a password reset costs you a customer who already pays you, cannot get into their account, and now needs support. Same score, completely different arithmetic — and any single cutoff is wrong for one of them by construction.&lt;/p&gt;

&lt;p&gt;Two things I would argue for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work out the exchange rate before the number.&lt;/strong&gt; How many real customers will you turn away to stop one bad signup? You do not need precision, but you need to know which way it leans, and that falls out of the two costs rather than out of a vendor's docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three bands, not two.&lt;/strong&gt; One line forces every borderline case into one of two wrong answers, and borderline is exactly where real customers and attackers overlap. A middle band routes ambiguity to a step-up — an emailed code, an SMS — which is cheap when you are right and recoverable when you are wrong.&lt;/p&gt;

&lt;p&gt;The part I find most people have not solved: how do you measure the mistake you cannot see? Fraud that gets through arrives as a chargeback. A customer wrongly refused closes the tab, and nothing in your dashboard records they existed.&lt;/p&gt;

&lt;p&gt;How are you counting that one?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
