<?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: Vinicius</title>
    <description>The latest articles on DEV Community by Vinicius (@viniciusbig).</description>
    <link>https://dev.to/viniciusbig</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%2Fuser%2Fprofile_image%2F783142%2F6addafad-49c9-448d-ab78-917c81893b3b.jpeg</url>
      <title>DEV Community: Vinicius</title>
      <link>https://dev.to/viniciusbig</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/viniciusbig"/>
    <language>en</language>
    <item>
      <title>The Active Flag Trap: unvalidated-but-logged-in in CakeDC/Users</title>
      <dc:creator>Vinicius</dc:creator>
      <pubDate>Thu, 20 Aug 2026 21:44:16 +0000</pubDate>
      <link>https://dev.to/viniciusbig/the-active-flag-trap-unvalidated-but-logged-in-in-cakedcusers-c8a</link>
      <guid>https://dev.to/viniciusbig/the-active-flag-trap-unvalidated-but-logged-in-in-cakedcusers-c8a</guid>
      <description>&lt;p&gt;If you ship email validation with &lt;a href="https://github.com/CakeDC/users" rel="noopener noreferrer"&gt;CakeDC/Users&lt;/a&gt;, you eventually hit a question the plugin quietly hands back to you: &lt;em&gt;what should happen when someone registers, never clicks the validation link, and then tries to log in?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The honest answer is that CakeDC/Users doesn't decide for you. Out of the box you get a database column, a couple of behaviors, and a set of events — but the &lt;em&gt;experience&lt;/em&gt; is yours to assemble. Get it wrong and you land in one of two bad places: a user silently logged in without ever validating, or a user who typed the right password and is told &lt;em&gt;"username or password is incorrect."&lt;/em&gt; Neither is what you want.&lt;/p&gt;

&lt;p&gt;This post walks through why that happens in v16, and a clean way to wire the flow using the events the plugin already dispatches — no core hacks, no schema surgery.&lt;/p&gt;

&lt;h2&gt;
  
  
  One flag, two meanings
&lt;/h2&gt;

&lt;p&gt;Everything starts with a single boolean column on the &lt;code&gt;users&lt;/code&gt; table: &lt;code&gt;active&lt;/code&gt;. When email validation is on, registration creates the account with &lt;code&gt;active = 0&lt;/code&gt; and only flips it to &lt;code&gt;1&lt;/code&gt; when the user clicks the link in the validation email. You can trace it in &lt;code&gt;BaseTokenBehavior::_updateActive()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// $user['validated'] is a transient flag set to false during register()&lt;/span&gt;
&lt;span class="nv"&gt;$emailValidated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'validated'&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="nv"&gt;$emailValidated&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$validateEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// registered → inactive + token emailed&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;updateToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tokenExpiration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// clicked the link → active&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'activation_date'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DateTime&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;Notice there is no separate &lt;code&gt;validated&lt;/code&gt; column in the database — &lt;code&gt;$user['validated']&lt;/code&gt; is a transient property used only during registration. The &lt;em&gt;persisted&lt;/em&gt; truth is &lt;code&gt;active&lt;/code&gt;, and it is doing two jobs at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Has this person confirmed their email?"&lt;/strong&gt; — set by the validation flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Is this account enabled?"&lt;/strong&gt; — the thing an admin toggles to ban or suspend someone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That conflation is the root of everything below. Hold onto it; we'll come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the finder decides who exists
&lt;/h2&gt;

&lt;p&gt;Login in CakeDC/Users runs through &lt;code&gt;cakephp/authentication&lt;/code&gt;'s password identifier, which resolves the user with a &lt;em&gt;finder&lt;/em&gt;. The finder you choose changes the entire shape of the problem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;finder&lt;/th&gt;
&lt;th&gt;Behaviour for an inactive account (&lt;code&gt;active = 0&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'active'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;WHERE active = 1&lt;/code&gt; clause means the row is &lt;strong&gt;never found&lt;/strong&gt;. The password is never checked. The user gets the generic &lt;em&gt;"username or password is incorrect"&lt;/em&gt; — even with the &lt;strong&gt;correct&lt;/strong&gt; password.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'all'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The row &lt;strong&gt;is found&lt;/strong&gt; and the password &lt;strong&gt;is&lt;/strong&gt; verified. With a correct password the identifier succeeds — so an unvalidated user is technically &lt;strong&gt;authenticated&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the counter-intuitive part. It's tempting to reach for &lt;code&gt;finder =&amp;gt; 'active'&lt;/code&gt; — surely inactive users just shouldn't log in? But that choice produces the second bad experience: a real user, with the right password, whose only sin is not clicking an email that probably landed in spam, is told their credentials are wrong. They'll try again, reset their password, and file a support ticket. They have no way of knowing the real problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common myth:&lt;/strong&gt; &lt;em&gt;"&lt;code&gt;finder =&amp;gt; 'all'&lt;/code&gt; is there to prevent user enumeration."&lt;/em&gt; It isn't. The form login failure maps &lt;em&gt;both&lt;/em&gt; "user not found" and "wrong password" to the same generic message anyway (&lt;code&gt;LoginComponentLoader::forForm&lt;/code&gt; wires no distinct not-active message). So &lt;code&gt;'active'&lt;/code&gt; leaks nothing — it just also blocks the legitimate not-yet-validated user with a confusing message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The better flow: authenticate, then decide
&lt;/h2&gt;

&lt;p&gt;The move that fixes both bad outcomes: keep &lt;code&gt;finder =&amp;gt; 'all'&lt;/code&gt; so the password &lt;em&gt;is&lt;/em&gt; verified, then intercept the result &lt;strong&gt;after&lt;/strong&gt; authentication and branch on &lt;code&gt;active&lt;/code&gt;. That's the only point where you can tell "wrong password" apart from "correct password, not validated" — and give each a truthful message.&lt;/p&gt;

&lt;p&gt;CakeDC/Users hands you the exact hook. &lt;code&gt;LoginComponent::afterIdentifyUser()&lt;/code&gt; dispatches &lt;code&gt;Users.Authentication.afterLogin&lt;/code&gt;, and &lt;strong&gt;if a listener returns an array, that array becomes the redirect&lt;/strong&gt;, short-circuiting the normal login redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EventInterface&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user'&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="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'active'&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;// active → let normal login proceed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Inactive account must never keep a session. The auth middleware&lt;/span&gt;
    &lt;span class="c1"&gt;// already persisted the identity — drop it right back.&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Authentication&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Flash'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// drop the "Welcome" flash&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Configure&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Users.Email.validate'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Flash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'Your account is not validated yet. Click the link in the '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
            &lt;span class="s1"&gt;'email we sent you — check spam, or request a new one below.'&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="s1"&gt;'action'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'resendTokenValidation'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// validation off ⇒ active=0 can only mean an admin disabled the account&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Flash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Your account is disabled. Please contact an administrator.'&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="s1"&gt;'action'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'login'&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;Two details make this safe. First, the auth middleware has &lt;em&gt;already&lt;/em&gt; written the identity to the session by the time your action runs, so simply redirecting isn't enough — you must call &lt;code&gt;$this-&amp;gt;Authentication-&amp;gt;logout()&lt;/code&gt; to drop it. An unvalidated account should never hold a session, not even for one request. Second, &lt;code&gt;LoginComponent&lt;/code&gt; queues a "Welcome" success flash &lt;em&gt;before&lt;/em&gt; this event fires, so you clear it before setting your own.&lt;/p&gt;

&lt;p&gt;Here are the three outcomes at &lt;code&gt;/login&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credentials&lt;/th&gt;
&lt;th&gt;&lt;code&gt;active&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;correct password&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Logged in.&lt;/strong&gt; Normal redirect to the dashboard.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;correct password&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Session dropped.&lt;/strong&gt; Redirected to the resend page with a specific "validate your account" message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wrong password / no such user&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Generic error.&lt;/strong&gt; &lt;em&gt;"Username or password is incorrect."&lt;/em&gt; No session, no enumeration leak.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this doesn't leak:&lt;/strong&gt; the specific "not validated" message only ever appears &lt;em&gt;after&lt;/em&gt; a correct password — i.e. to someone who has already proven the account exists. Wrong-password and unknown-user still collapse into one generic message. You get honesty for real users and opacity for attackers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  After register: tell them to check their email
&lt;/h2&gt;

&lt;p&gt;The other half of the problem is that a fresh registrant has no idea a validation email is even coming. By default the plugin flashes &lt;em&gt;"please validate your account before log in"&lt;/em&gt; and drops them on the login screen — where the resend option is nowhere in sight. So send them somewhere that explains the next step.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RegisterTrait::_afterRegister()&lt;/code&gt; dispatches &lt;code&gt;Users.Global.afterRegister&lt;/code&gt;. Careful: unlike the login hook, this one only honours a return value that is a &lt;strong&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/strong&gt; instance — an array is ignored. So return a redirect response, not a URL array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EventInterface&lt;/span&gt; &lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Response&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="nc"&gt;Configure&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Users.Email.validate'&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// nothing to validate → default "log in" redirect stands&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Flash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'Almost done! We sent a validation link to your email. Click it '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
        &lt;span class="s1"&gt;'to activate your account — check spam, or request a new link below.'&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'action'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'resendTokenValidation'&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;Both listeners land on the same destination: &lt;code&gt;resendTokenValidation&lt;/code&gt;. That action already ships with the plugin (in &lt;code&gt;UserValidationTrait&lt;/code&gt;) — it takes an email or username, regenerates the token, and re-sends the validation email, refusing if the account is already active. You just need a themed template for it. Wiring both events is a two-line job in your controller's &lt;code&gt;initialize()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEventManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;UsersPlugin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EVENT_AFTER_LOGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EventInterface&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;afterLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEventManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;UsersPlugin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EVENT_AFTER_REGISTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;EventInterface&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;afterRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mind the return contracts:&lt;/strong&gt; &lt;code&gt;afterLogin&lt;/code&gt; must return an &lt;strong&gt;array&lt;/strong&gt; (&lt;code&gt;afterIdentifyUser&lt;/code&gt; checks &lt;code&gt;is_array()&lt;/code&gt;). &lt;code&gt;afterRegister&lt;/code&gt; must return a &lt;strong&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;_afterRegister&lt;/code&gt; checks &lt;code&gt;instanceof Response&lt;/code&gt;). Same pattern, two different types — mixing them up gives you a listener that silently does nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The caveat you've only postponed
&lt;/h2&gt;

&lt;p&gt;Remember the two jobs &lt;code&gt;active&lt;/code&gt; is doing? This flow leans on the "email confirmed" meaning, but the column can't tell that apart from the "admin disabled this account" meaning. Both are &lt;code&gt;active = 0&lt;/code&gt;. Which means:&lt;/p&gt;

&lt;p&gt;A user an admin &lt;em&gt;deliberately banned&lt;/em&gt; can visit the resend page, request a "validation" email, click the link, and &lt;strong&gt;re-activate their own account&lt;/strong&gt;. The &lt;code&gt;resendTokenValidation&lt;/code&gt; action only checks that the account isn't already active — it can't know &lt;em&gt;why&lt;/em&gt; it's inactive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The real fix:&lt;/strong&gt; split the concepts. Add a dedicated &lt;code&gt;enabled&lt;/code&gt; column (admin on/off), keep &lt;code&gt;active&lt;/code&gt; for email confirmation, and require &lt;em&gt;both&lt;/em&gt; in your login finder. Now "never validated" and "banned" are distinct states, and a resend can't undo a ban. Until you do that, only offer resend when email validation is on — and bounce a disabled account (validation off) straight back to login with a neutral message, as the listener above does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For an existing app, adding a column is a migration and a finder tweak. For a greenfield build it's nearly free — so if you're starting fresh, separate them from day one and skip the trap entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep &lt;code&gt;finder =&amp;gt; 'all'&lt;/code&gt;.&lt;/strong&gt; You need the password verified to distinguish "wrong password" from "not validated." It costs you no enumeration protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branch after authentication, not before.&lt;/strong&gt; The &lt;code&gt;Users.Authentication.afterLogin&lt;/code&gt; event is where the truthful decision lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always drop the session&lt;/strong&gt; for an inactive account — the middleware already created it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guide the registrant.&lt;/strong&gt; Redirect after register to a page that explains the email and offers a resend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust one flag with two meanings.&lt;/strong&gt; An &lt;code&gt;enabled&lt;/code&gt; column is the difference between "restart validation" and "let a banned user back in."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires forking the plugin. CakeDC/Users gives you the seams — the events, the finder switch, the built-in resend action. It just leaves the last mile, the part your users actually feel, for you to build.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built against CakeDC/Users v16 on CakePHP 5. The event names and method signatures referenced here are from that line; earlier majors dispatch similar events under different names. Spot something I got wrong? Drop a comment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cakephp</category>
      <category>php</category>
      <category>authentication</category>
      <category>webdev</category>
    </item>
    <item>
      <title>FormValidation is discontinued — here's the TypeScript successor I built</title>
      <dc:creator>Vinicius</dc:creator>
      <pubDate>Thu, 18 Jun 2026 22:32:52 +0000</pubDate>
      <link>https://dev.to/viniciusbig/formvalidation-is-discontinued-heres-the-typescript-successor-i-built-4804</link>
      <guid>https://dev.to/viniciusbig/formvalidation-is-discontinued-heres-the-typescript-successor-i-built-4804</guid>
      <description>&lt;p&gt;If you've ever worked on a complex form without a framework, there's a good chance you've used or at least heard of &lt;a href="https://formvalidation.io/" rel="noopener noreferrer"&gt;FormValidation&lt;/a&gt;. It was the go-to solution for plugin-based, framework-agnostic form validation for years.&lt;/p&gt;

&lt;p&gt;Then it went silent. No updates, no responses to issues. Just an archived library slowly rotting against modern JavaScript.&lt;/p&gt;

&lt;p&gt;I needed a replacement for a project. I couldn't find one that matched the flexibility of the plugin model, so I built &lt;strong&gt;Validare&lt;/strong&gt; — a modern TypeScript rewrite of the same architecture, from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Validare is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@validare/core&lt;/code&gt;&lt;/strong&gt; is a form validation library with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;51 built-in validators&lt;/strong&gt; — from &lt;code&gt;notEmpty&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; to &lt;code&gt;iban&lt;/code&gt;, &lt;code&gt;creditCard&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;vin&lt;/code&gt;, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17 plugins&lt;/strong&gt; — UI feedback, accessibility, CSS framework integrations, async flows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript-first&lt;/strong&gt; — full type safety, no &lt;code&gt;@types/*&lt;/code&gt; packages needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — no jQuery, no frameworks, no polyfills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESM + CJS + UMD&lt;/strong&gt; — works everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core idea is the same as FormValidation: a small engine that does nothing on its own, extended entirely through plugins. You add exactly what you need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @validare/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"myForm"&lt;/span&gt; &lt;span class="na"&gt;novalidate&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"vd-plugins-message-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validare&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SubmitButton&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@validare/core&lt;/span&gt;&lt;span class="dl"&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;fv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myForm&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;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;notEmpty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please enter a valid email address&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;submitButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SubmitButton&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myForm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;result&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;fv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 'Valid' | 'Invalid' | 'NotValidated'&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Valid&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="c1"&gt;// safe to submit&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;That's it. The &lt;code&gt;Trigger&lt;/code&gt; plugin wires up DOM events, &lt;code&gt;Message&lt;/code&gt; renders error messages, and &lt;code&gt;SubmitButton&lt;/code&gt; disables the button during validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The plugin model
&lt;/h2&gt;

&lt;p&gt;Plugins are where Validare earns its keep. Each one is a focused, composable unit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Trigger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validates on DOM events (&lt;code&gt;blur&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;change&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renders error messages in the DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Icon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows ✓ / ✗ icons next to fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bootstrap5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies &lt;code&gt;is-valid&lt;/code&gt; / &lt;code&gt;is-invalid&lt;/code&gt; classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Tailwind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies configurable Tailwind utility classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Aria&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adds &lt;code&gt;aria-invalid&lt;/code&gt; and &lt;code&gt;aria-describedby&lt;/code&gt; automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops at the first failing validator per field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Excluded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skips disabled or hidden fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Declarative&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configures validators via &lt;code&gt;data-vd-*&lt;/code&gt; HTML attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PasswordStrength&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scores password strength (0–4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CharCounter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live character counter with overflow state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Summary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renders a consolidated error list in a container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Transformer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Normalises field values before validation (trim, strip, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You compose only what your project needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Bootstrap5&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Aria&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;excluded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Excluded&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;h2&gt;
  
  
  51 validators, organized by category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Core (23):&lt;/strong&gt; &lt;code&gt;notEmpty&lt;/code&gt;, &lt;code&gt;blank&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;creditCard&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;digits&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;numeric&lt;/code&gt;, &lt;code&gt;regexp&lt;/code&gt;, &lt;code&gt;uri&lt;/code&gt;, &lt;code&gt;identical&lt;/code&gt;, &lt;code&gt;different&lt;/code&gt;, &lt;code&gt;between&lt;/code&gt;, &lt;code&gt;greaterThan&lt;/code&gt;, &lt;code&gt;lessThan&lt;/code&gt;, &lt;code&gt;stringLength&lt;/code&gt;, &lt;code&gt;stringCase&lt;/code&gt;, &lt;code&gt;choice&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;callback&lt;/code&gt;, &lt;code&gt;promise&lt;/code&gt;, &lt;code&gt;remote&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format &amp;amp; Encoding:&lt;/strong&gt; &lt;code&gt;base64&lt;/code&gt;, &lt;code&gt;hex&lt;/code&gt;, &lt;code&gt;mac&lt;/code&gt;, &lt;code&gt;bic&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Financial:&lt;/strong&gt; &lt;code&gt;iban&lt;/code&gt;, &lt;code&gt;vat&lt;/code&gt;, &lt;code&gt;cusip&lt;/code&gt;, &lt;code&gt;isin&lt;/code&gt;, &lt;code&gt;sedol&lt;/code&gt;, &lt;code&gt;grid&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publication:&lt;/strong&gt; &lt;code&gt;ean&lt;/code&gt;, &lt;code&gt;isbn&lt;/code&gt;, &lt;code&gt;ismn&lt;/code&gt;, &lt;code&gt;issn&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device &amp;amp; Vehicle:&lt;/strong&gt; &lt;code&gt;imei&lt;/code&gt;, &lt;code&gt;imo&lt;/code&gt;, &lt;code&gt;meid&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;vin&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tax &amp;amp; Business:&lt;/strong&gt; &lt;code&gt;ein&lt;/code&gt;, &lt;code&gt;rtn&lt;/code&gt;, &lt;code&gt;siren&lt;/code&gt;, &lt;code&gt;siret&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity &amp;amp; Geographic:&lt;/strong&gt; &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;zipCode&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Async validation and remote checks
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;remote&lt;/code&gt; validator hits an endpoint and expects &lt;code&gt;{ valid: boolean }&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;notEmpty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Username is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/check-username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This username is already taken&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="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;Or write your own async validator with &lt;code&gt;callback&lt;/code&gt; or &lt;code&gt;promise&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;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="s2"&gt;`/api/validate?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="p"&gt;}&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&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="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Writing a custom validator
&lt;/h2&gt;

&lt;p&gt;Every validator is a zero-argument factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ValidatorFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@validare/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;noSpaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ValidatorFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&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;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No spaces allowed&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it directly in your config — no registration needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;noSpaces&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./validators/noSpaces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;notEmpty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;noSpaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;noSpaces&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="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Coming from FormValidation?
&lt;/h2&gt;

&lt;p&gt;The architecture is intentionally familiar. The main differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Package name: &lt;code&gt;@validare/core&lt;/code&gt; (not &lt;code&gt;@form-validation/cjs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CSS classes: &lt;code&gt;vd-plugins-*&lt;/code&gt; instead of &lt;code&gt;fv-plugins-*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HTML attributes (Declarative plugin): &lt;code&gt;data-vd-*&lt;/code&gt; instead of &lt;code&gt;data-fv-*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Native TypeScript — no extra type packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A full migration guide is available in &lt;a href="https://github.com/validarejs/validare/blob/main/MIGRATING.md" rel="noopener noreferrer"&gt;MIGRATING.md&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;code&gt;npm install @validare/core&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://validare.js.org" rel="noopener noreferrer"&gt;validare.js.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/validarejs/validare" rel="noopener noreferrer"&gt;github.com/validarejs/validare&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CDN: &lt;code&gt;https://unpkg.com/@validare/core/dist/index.umd.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback, issues, and PRs are very welcome. This is an active project and I'm happy to prioritize validators or plugins based on what people actually need.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
