
Security News
Re-Enabled GitHub Actions Expose Thousands of Repositories to Mini Shai-Hulud
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.
@authlocker/react
Advanced tools
Headless React primitives for AuthLocker: one OTP state machine for email and SMS, social sign-in triggers, and an adapter seam so the same components run against a Better Auth plugin or your own route handler.
Headless React primitives for AuthLocker: one OTP state machine that serves both email and SMS, social sign-in triggers, and an adapter seam so the same components run against a Better Auth plugin today and your own route handler tomorrow.
No styles, no markup opinions, no runtime dependency but React. Every part
carries data-state, so you can style the whole flow in plain CSS.
pnpm add @authlocker/react
POST /api/v1/verify/* authenticates with your client secret, and
AuthLocker sends no CORS headers, so a browser can never call it — not with a
clever fetch, not at all. Every consumer needs a server of their own in front
of it. That server is the only thing that differs between setups, so it is the
only thing these components are parameterised by.
// Ships now — the Better Auth plugin.
import { createBetterAuthAdapter } from "@authlocker/react";
import { authClient } from "@/lib/auth-client";
export const adapter = createBetterAuthAdapter(authClient);
// The same contract against your own catch-all. See the note below.
import { createAuthLockerAdapter } from "@authlocker/react";
export const adapter = createAuthLockerAdapter({ basePath: "/api/authlocker" });
Wrap the tree once:
"use client";
import { AuthLockerProvider } from "@authlocker/react/client";
export function Providers({ children }: { children: React.ReactNode }) {
return <AuthLockerProvider adapter={adapter}>{children}</AuthLockerProvider>;
}
Every hook and every part also takes an adapter prop, so one form can talk to
a different backend than the rest of the app without a second provider.
createAuthLockerAdapteris written but its server is not.@authlocker/nextand its/api/authlocker/[...path]handler are the next package. Until it lands, the four routes it expects are documented on the function itself and are a few lines each — proxyverify/start,verify/checkandverify/resendwith the HTTP Basic header, and redirectoauth/{provider}/startto the authorize endpoint. Forward AuthLocker's failure body unchanged:attemptsRemainingrides besideerror, not inside it, and re-wrapping it costs the UI its attempt counter.
"use client";
import { EmailOtp } from "@authlocker/react/client";
export function SignInForm() {
return (
<EmailOtp.Root template="signin" onVerified={(v) => console.log(v.status)}>
<EmailOtp.TargetField placeholder="you@example.com" />
<EmailOtp.StartTrigger>Send code</EmailOtp.StartTrigger>
<EmailOtp.CodeField />
<EmailOtp.VerifyTrigger>Verify</EmailOtp.VerifyTrigger>
<EmailOtp.ResendTrigger>
{({ secondsRemaining }) =>
secondsRemaining > 0 ? `Resend in ${secondsRemaining}s` : "Resend code"}
</EmailOtp.ResendTrigger>
<EmailOtp.Status>
{({ state, verification, attemptsRemaining }) =>
state === "awaiting_code"
? `We sent a code to ${verification?.to}. ${attemptsRemaining} tries left.`
: null}
</EmailOtp.Status>
<EmailOtp.Error />
</EmailOtp.Root>
);
}
Otp.Root renders a <div>, the triggers render <button>s, the fields
render <input>s. Nothing is hidden or shown for you — that is what
data-state is for:
[data-authlocker-otp][data-state="idle"] .code-row { display: none }
[data-authlocker-otp][data-state="verified"] .form { display: none }
.resend[data-disabled] { opacity: .5; pointer-events: none }
The same machine with channel: "sms", so PhoneOtp is a binding over
Otp, not a second implementation. TargetField picks up type="tel" from
the channel; the address must be E.164.
<PhoneOtp.Root>
<PhoneOtp.TargetField placeholder="+15551234567" />
<PhoneOtp.StartTrigger>Text me a code</PhoneOtp.StartTrigger>
<PhoneOtp.CodeField />
</PhoneOtp.Root>
SMS requires a client claimed into a namespace; the anonymous DCR client gets
SMS_REQUIRES_CLAIMED_CLIENT.
import { SignInWith, SignInWithGoogle, SignInWithGitHub } from "@authlocker/react/client";
<SignInWithGoogle>Continue with Google</SignInWithGoogle>
<SignInWithGitHub callbackURL="/dashboard">Continue with GitHub</SignInWithGitHub>
<SignInWith provider="google">Continue with Google</SignInWith>
provider is a string your adapter resolves, not one AuthLocker resolves.
Through createBetterAuthAdapter it is a Better Auth providerId, so
"google" needs a genericOAuth config registered under exactly that id, with
authorizationUrlParams: { provider: "google" } — AuthLocker's
authorization_endpoint is /oauth/authorize and refuses a request that names
no provider — and {baseURL}/api/auth/callback/google on the client's
registered redirect_uri list. Through createAuthLockerAdapter it is the
segment in oauth/{provider}/start, which your handler forwards.
A resolved beginSocialSignIn is never a signed-in user: the browser has left,
and the answer arrives at your callback route. onError fires only when the
navigation did not happen at all.
Composition is Base UI's render prop — pass an element, and the part's props
and handlers are merged onto it:
<SignInWithGoogle render={<Button variant="outline" />}>
Continue with Google
</SignInWithGoogle>
<Otp.CodeField render={<InputOTP maxLength={6} />} />
The merge rules, all of which exist because the naive version breaks something:
preventDefault()
cancels ours — that is how you veto a send;className concatenates and style merges, yours last;disabled is OR-ed, never overridden, so disabled={false} cannot re-open a
trigger the machine has closed;type and disabled are dropped when the target is a non-control element
such as <a>, where they are invalid and, in disabled's case, silently
ignored — the state moves to aria-disabled and the trigger re-checks it in
its own handler.Otp.CodeField and Otp.TargetField accept a value-based onChange as well
as a DOM event, which is what makes render={<InputOTP />} work.
Only the element form is supported, not Base UI's render={(props) => …}
function form. useOtp() covers that case with more control. And do not render
an <a href> through a button primitive to get a link that looks like a
button: that stamps role="button" on something which follows an href. Style a
plain <a> instead.
For markup you own entirely:
const otp = useOtp({ channel: "email", template: "signin" });
// otp.state, .target, .setTarget, .code, .setCode, .verification, .error,
// .attemptsRemaining, .resendsRemaining, .secondsRemaining,
// .canStart, .canVerify, .canResend, .start, .verify, .resend, .reset
const { signIn, state, error } = useSocialSignIn({ callbackURL: "/dashboard" });
idle → starting → awaiting_code → verifying → verified
↓ ↓
failed locked (+ resend cooldown timer)
| State | Means |
|---|---|
idle | Waiting for an address. |
starting | The send is in flight. |
awaiting_code | A code is out. The cooldown is counting down. |
verifying | The check is in flight. |
verified | Done — and verification.to is now the real address. |
failed | The flow cannot continue. reset() is the exit. |
locked | Five attempts spent. Terminal; resending does not recover it. |
Error handling is part of the contract, not a detail:
| Code | Effect |
|---|---|
INVALID_CODE | Stays in awaiting_code, clears the field, decrements attemptsRemaining. |
VERIFICATION_LOCKED | → locked. Terminal. |
VERIFICATION_EXPIRED | → idle, target kept, error kept so you can say why. |
VERIFICATION_ALREADY_VERIFIED, NOT_FOUND | → failed. |
RESEND_TOO_SOON | Restarts the cooldown from Retry-After, or the live deadline, or 60s. |
RESEND_LIMIT_REACHED | resendsRemaining → 0; the trigger stays closed. |
| anything else | Stays put — a throttle or a dropped connection is not a dead end. |
attemptsRemaining arrives as a sibling of error in AuthLocker's failure
body, not inside it. Both adapters know that. If you write your own, keep it.
Service constants, exported as VERIFICATION_CONSTANTS: a ten-minute TTL, five
attempts, three resends, a sixty-second resend interval, six-digit codes.
| Part | Element | Attributes |
|---|---|---|
Otp.Root | div | data-authlocker-otp, data-state, data-channel |
Otp.TargetField | input | data-state, data-channel, data-disabled |
Otp.StartTrigger | button | data-state, data-pending, data-disabled |
Otp.CodeField | input | data-state, data-complete, data-disabled |
Otp.VerifyTrigger | button | data-state, data-pending, data-disabled |
Otp.ResendTrigger | button | data-state, data-cooldown, data-exhausted, data-disabled |
Otp.Status | div | role="status", aria-live="polite", data-state |
Otp.Error | div | role="alert", data-code; renders nothing until something fails |
Otp.Root also takes children as a function of the machine, and
useOtpContext() lets you write a part we did not think of.
verification.to is masked (a**@example.com, +*******1234) on every
response but one: a successful check returns it in full, because proving
possession is what earns it back. It is fine to render. It does not belong in a
log line, an analytics event, or an error message — which is why nothing this
package throws ever quotes a payload.
| Import | Contains |
|---|---|
@authlocker/react | Types, AuthLockerError, the adapters. No React — safe in a server component. |
@authlocker/react/client | The components and hooks. "use client". |
MIT.
FAQs
Headless React primitives for AuthLocker: one OTP state machine for email and SMS, social sign-in triggers, and an adapter seam so the same components run against a Better Auth plugin or your own route handler.
The npm package @authlocker/react receives a total of 0 weekly downloads. As such, @authlocker/react popularity was classified as not popular.
We found that @authlocker/react demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.

Research
/Security News
The compromise affects MemTensor's MemOS, an open source memory framework for large language models (LLMs) and AI agents. Both npm package @memtensor/memos-cloud-openclaw-plugin and the PyPI package MemoryOS are compromised. They drop cross-platform Go binaries that exfiltrate developer secrets.