npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@proteles/alpine-ajax

v0.1.1

Published

Alpine AJAX server helpers for Proteles authentication: target-aware sign-in prompts, route guards and session helpers. Server-only — Alpine AJAX needs no client-side auth script.

Readme

@proteles/alpine-ajax

Server helpers for Alpine AJAX apps authenticating with Proteles.

Alpine AJAX keeps no client auth state — the server renders the HTML — so this package is server-only. There is no browser bundle and no client-side auth script to keep in sync, which is rather the point of choosing it.

Install

npm install @proteles/alpine-ajax

The one rule

Your sign-in and sign-out controls carry no x-target.

<!-- right: a real navigation to the authorization server -->
<a href="/api/auth/login">Sign in</a>
<form method="post" action="/api/auth/logout"><button>Sign out</button></form>

<!-- wrong: fetches the sign-in page and merges it into #app -->
<a href="/api/auth/login" x-target="app">Sign in</a>

That is not a style preference, and it is worth understanding why, because it is the difference between this package and its htmx sibling.

htmx has the same underlying problem — XHR follows redirects, so an unauthenticated fragment request ends up swapping a login page into a table cell — and htmx supplies an escape hatch: answer 200 with HX-Redirect and the browser navigates. @proteles/htmx is essentially that one rewrite.

Alpine AJAX has no such hatch. Its own reference is explicit: "The JavaScript Fetch API follows all redirects transparently, so Alpine AJAX cannot distinguish between 300 class status codes." No response header makes it navigate. A server cannot force a full page load.

So this package does not try. Signing in is a navigation, so it stays one.

What it does instead

Alpine AJAX merges response bodies for every status, including 4xx. So the right answer to "this fragment needs a session" is not a redirect that fetch will silently follow — it is a 401 whose body is addressed to the ids the caller asked for in X-Alpine-Target.

The user sees "Your session has expired — Sign in" exactly where the content would have gone. No login page in a table cell, no address bar surprise, and the status is a real one that x-target.4xx, an ajax:error listener and your logs can all key on.

import { protelesAuthHandler, requireUser } from "@proteles/alpine-ajax";
import { sendNodeResponse } from "@proteles/bff";

// the four auth routes
if (url.pathname.startsWith("/api/auth/")) {
  return sendNodeResponse(res, await protelesAuthHandler({ node: { req } }));
}

// a route that renders a fragment
const auth = await requireUser({ node: { req } });
if (auth.response) return sendNodeResponse(res, auth.response);
html(res, `<div id="profile">Hello ${auth.user.email}</div>`);

The id in the body matters. Alpine merges by id, and a response that does not contain the requested one fires ajax:missing and swaps nothing — which, to the person clicking, is indistinguishable from the click not working. requireUser takes the ids from the request rather than guessing.

API

| Export | What it does | | --- | --- | | protelesAuthHandler(source, config?) | Handles login, callback, logout, me. Redirects stay redirects. | | guardRequest(source, options?, config?) | Middleware guard. Returns a response to send, or undefined to continue. | | requireUser(source, options?, config?) | In-route guard. Returns { user } or { response }. | | getUser(source, config?) | The sanitized user (no tokens), or null. | | attachUser(source, config?) | Same, cached on the framework's context bag. | | getSession(source, config?) | The full session including tokens — server-side only. | | isAlpineRequest(source) | Whether X-Alpine-Request: true is set. | | alpineTargets(source) | The ids from X-Alpine-Target. | | signInFragment(targets, options) | The body requireUser sends, if you want to build your own. |

options on the two guards takes message and linkText to change the copy, and guardRequest also takes publicPaths.

Reacting on the client

The 401 is a normal Alpine response, so the usual hooks work:

<div id="profile" x-init x-target="profile"
     @ajax:error="if ($event.detail.status === 401) location.reload()">
</div>

Reloading is often the friendliest thing: the guarded page then does a real navigation to sign-in, with returnTo already set.

Security notes

X-Alpine-Target is a request header, so it is attacker-controlled, and it is interpolated into HTML. Ids that could not be Alpine targets anyway — anything outside [A-Za-z][\w:.-]*, or longer than 128 characters — are dropped rather than escaped, which removes the question instead of answering it. Everything that is interpolated is escaped as well.

The sign-in response is Cache-Control: no-store. It is per-session by definition, and a shared cache handing it to the next visitor would be worse than useless.

Develop

npm run build      # tsc
npm run typecheck
npm test

The live loop against a real authorization server runs in the repository's e2e: sdk/scripts/verify-alpine-ajax.mjs.