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

@alxia/janus

v0.4.2

Published

Identities and sessions for alxia on @softistx/janus: the user typed in the context, the session cookie kept and renewed, a typed 401

Readme

@alxia/janus

Identities, sessions and permissions for alxia, on @softistx/janus: your process, your database. The user typed in the context, the session cookie kept and renewed, janus's refusals answered with their status and a safe body. No dependency.

bun add @alxia/janus @softistx/janus @alxia/core
bun add -d typescript

@softistx/janus is private, on GitHub Packages. Map its scope in your bunfig.toml and export a token that reads packages (read:packages):

[install.scopes]
"@softistx" = { url = "https://npm.pkg.github.com/", token = "$GITHUB_TOKEN" }

It mirrors @softistx/janus-hono: the same cookie, the same rules, the same bodies.

A BFF that calls a janus API, rather than running @softistx/janus itself, wants @alxia/react-router/session.

Usage

import { alxia, validate } from '@alxia/core';
import { janusErrors, session } from '@alxia/janus';
import { createMemoryStores, janus, scryptHasher } from '@softistx/janus';
import { z } from 'zod';

const accounts = janus({
	user: z.object({ email: z.email(), name: z.string() }),
	password: { login: 'email' },
	store: createMemoryStores(),       // your database's adapter in production
	hasher: scryptHasher(),
});

const SignIn = z.object({ email: z.string(), password: z.string() });

const app = alxia()
	.use(janusErrors(), session(accounts))                // errors first, then the session: anonymous may sign in
	.post('/signin', validate({ body: SignIn }), async ({ body, auth, reply }) => {
		const signedIn = await accounts.signIn(body);
		return reply.ok({ id: auth.send(signedIn).id });  // the token in the cookie
	})
	.post('/signout', async ({ auth, reply }) => reply.ok(await auth.signOut()))
	.use(session(accounts, { required: true }))           // the routes after it, and any unmatched request
	.get('/me', ({ user, reply }) => reply.ok(user));     // user typed by the schema

janusErrors() and session() are middlewares. janusErrors() answers what is thrown behind it, so give it to use before session().

session(accounts, options?)

The routes after it read user, session and auth. With required: true, an anonymous request is a 401 { error: 'unauthenticated' } and user is never null. On the app, a required session also runs on a request no route matches: an anonymous request to a missing path gets the 401, not the 404. Put it in a group to guard only some routes; without, it is null for an anonymous request. A boolean known only at run time types both: the 401, and a user that may be null. type narrows to one user type of a multi-type janus(); device names the device cookie.

The session is read from Authorization: Bearer, X-Session-Token, or the cookie. One renewed in passing is sent again as a cookie after the route — only to a request that presented it as one, and never over a session cookie the route set itself. An outage is not anonymous: a store that cannot answer is STORE_FAILED, answered 503 by janusErrors().

ctx.auth: signing in and out

Sign-in stays on your janus() instance — accounts.signIn, accounts.signUp, accounts.patient.signIn, a code, a link — and ctx.auth does the request's part, bound to it:

| ctx.auth. | | | --- | --- | | send(signedIn) | sets the session cookie (and the device cookie, with a deviceToken) and returns the user: the token is never in a body | | signOut() | revokes the request's session and clears its cookie, whatever the answer | | device | the device cookie's value, or null, for signIn(…, { device: ctx.auth.device }) with janus({ devices }) |

Put the sign-in routes behind a session(accounts) that is not required: behind required: true, an anonymous request — anyone signing in — is a 401. Outside a route (a job, a test), sendSession(ctx, accounts, signedIn), signOut(ctx, accounts) and deviceOf(ctx) are the same functions, unbound.

janusErrors(options?)

A try/catch middleware: every JanusError thrown behind it — by session(), permission() or a route — is answered with janus's status and a body holding its code and only what a client can act on — the issues, a minimum length, attempts left, seconds to wait (with Retry-After). Never a login, a reason or a cause. report(error, ctx) is called for the 5xx. Anything else goes on, thrown, to the middlewares before it. On an app with alxia({ errors: 'problem' }), it answers an RFC 9457 problem instead, the code and the rest of that body its extensions; a required session()'s 401 and permission()'s 401, 404 and 403 follow the app's format too. Declared after session(), it would not see the session's STORE_FAILED: it goes first, and after the observers (logger(), secureHeaders()), so that they see its reply. An observer settles next() without swallowing the error, so janusErrors() before one, or before createI18n(), still answers it.

| status | codes | | --- | --- | | 400 | USER_INVALID, PASSWORD_TOO_SHORT, TOKEN_*… | | 401 | CREDENTIALS_INVALID, CODE_INVALID | | 403 | USER_INACTIVE, STEP_UP_REQUIRED | | 404 | NOT_FOUND | | 409 | LOGIN_TAKEN, VERSION_CONFLICT, SECOND_FACTOR_* | | 429 | MAIL_THROTTLED (@softistx/janus 0.18+), with retryAfter and Retry-After | | 500 | PERMISSION_DEPTH | | 501 | UNSUPPORTED | | 503 | STORE_FAILED |

permission(access, permission, type, load, options?)

A guard on @softistx/janus/permissions: the routes after it run only if the subject — the session's user, or options.subject(ctx) — holds permission on the object load finds, which they read as object. Anonymous is a 401, nothing loaded a 404, a denial a 403, each typed. Scope it with group:

import { alxia } from '@alxia/core';
import { byParam, janusErrors, permission, session } from '@alxia/janus';
import { createMemoryStores, janus, scryptHasher } from '@softistx/janus';
import { createMemoryRelations, defineModel, permissions } from '@softistx/janus/permissions';
import { z } from 'zod';

const relations = createMemoryRelations();
const accounts = janus({
	user: z.object({ email: z.email() }),
	password: { login: 'email' },
	store: createMemoryStores(),
	relations,
	hasher: scryptHasher(),
});
const access = permissions({
	model: defineModel({
		subjects: accounts.types,
		types: { record: { related: { owners: ['user'] }, permits: { view: ['owners'] } } },
	}),
	store: relations,
});

const records = new Map([['r1', { id: 'r1', title: 'Blood test' }]]);
const findRecord = (id: string) => records.get(id) ?? null;

const app = alxia()
	.use(janusErrors())
	.use(session(accounts))
	.group('/records/:id', (record) =>
		record
			.use(permission(access, 'view', 'record', byParam('id', findRecord)))
			.get('/', ({ object, reply }) => reply(200, object)), // object: what findRecord found
	);

byParam(name, find) loads by a path parameter. A permission whose condition needs a context takes ctx: (ctx, object) => …, required by the types exactly then.

Reading the app's context

Annotate load, subject or ctx's parameter to read what an earlier middleware added. The guard then requires it: an app that does not give it before the guard cannot use it.

import type { BaseContext } from '@alxia/core';

const byTenant = permission(
	access,
	'view',
	'record',
	({ tenant, pathParams }: BaseContext & { tenant: { records: Map<string, { id: string; title: string }> } }) =>
		tenant.records.get(pathParams['id'] ?? '') ?? null,
);

alxia().plugin(tenancy).use(session(accounts)).use(byTenant); // tenancy derives tenant
alxia().use(session(accounts)).use(byTenant); // a compile error: this app gives no `tenant`

A callback annotated any would require nothing, so the guard is refused on every app: annotate what it reads, or leave it unannotated.

API

| export | | | --- | --- | | session(accounts, options?) | the middleware: user, session, auth | | SessionOptions | its options: type, required, device | | RequestAuth, SessionOpened | the type of ctx.auth: send, signOut, device; and what send takes: token, session and user from @softistx/janus's SignedIn, and deviceToken? | | sendSession, signOut, deviceOf, sendDevice | the cookies, outside a route | | DEVICE_COOKIE | the device cookie's name, janus-device: the one @softistx/janus-hono uses, so a device one remembers the other does too | | SendSessionOptions, DeviceCookieOptions | their options: device; name, domain, path, sameSite, secure, maxAge | | janusErrors(options?) | the middleware: janus's refusals answered; use it before session() | | JanusErrors, SessionMiddleware | the types of what janusErrors() and session() make | | JanusErrorsOptions | its options: report, called with every error answered 5xx | | permission(…), byParam(…) | the guard | | PermissionOptions, OptionsArgs | its options: subject, ctx; and the rest of its arguments, the options required exactly when the permission has a condition; both take what subject and ctx read beyond BaseContext as their last two, defaulted, parameters | | bodyOf, statusOf | a refusal's body and status | | UnauthenticatedBody, JanusErrorBody, PermissionRefusedBody | their types | | UnauthenticatedProblem, JanusErrorProblem, PermissionRefusedProblem | the same answers under @alxia/core's alxia({ errors: 'problem' }): RFC 9457 problems, janusErrors()'s with its code and body as extensions | | Auth, UserOfAuth, ObjectData, Awaitable | the part of janus() this package calls, the users it knows, an object as the application loads it, a value or its promise |

Documentation

  • Guide: a page per area — sessions, signing in and out with ctx.auth and the device cookie, janus's errors and their statuses, and the permission guard, reading what an earlier middleware added included.
  • Troubleshooting: an error message, or a request anonymous, refused or a 404 when it should not be, and what to do about it.
  • Roadmap: what is coming, and what is not planned.
  • Recipes: Authenticate requests, Answer errors consistently.