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

@skylab-kulubu/inscribed-auth

v0.3.0

Published

Skylab's opt-in NextAuth + Keycloak adapter (auth + service token) for the inscribed CMS.

Readme

@skylab-kulubu/inscribed-auth

Skylab's opt-in NextAuth + Keycloak adapter for the inscribed CMS.

inscribed is auth- and vendor-neutral: out of the box it runs read-only/public. This package is the Skylab layer that plugs admin auth and the build-time service token into it, so a new Skylab Next.js app gets editing + sync by installing one package and setting env vars — no hand-copied auth files.

It ships two seam adapters:

| Seam | What this package provides | | --- | --- | | Auth | NextAuth options (JWT access/refresh persistence, silent refresh, Keycloak client- and realm-role extraction), the withCmsAuth adapter, the NextAuthCmsProvider client wrapper, and a one-route auto-signin handler. | | Service token | Keycloak client-credentials token for SSR content fetch + the cms-sync CLI. |

Transport is not provided — Skylab uses inscribed's default REST transport.

Install

npm install @skylab-kulubu/inscribed-auth

Peer dependencies (you already have most in a Next app):

npm install inscribed next next-auth@^4 react react-dom

Keep the app CommonJS (do not set "type": "module" in the app's package.json). next-auth v4's Keycloak provider only resolves through Webpack's CJS/ESM interop; an ESM app breaks it. The one exception is cms.config.mjs below, which is .mjs on purpose.

Entry points

| Import | Use from | Exports | | --- | --- | --- | | @skylab-kulubu/inscribed-auth | Client ("use client") | NextAuthCmsProvider | | @skylab-kulubu/inscribed-auth/server | Server only | createCmsAuthOptions, withCmsAuth, isCmsAdmin, readCmsAuthMeta, getClientCredentialsToken, debugServiceTokenClaims | | @skylab-kulubu/inscribed-auth/signin | Server route | GET, createSignInRoute | | @skylab-kulubu/inscribed-auth/config | cms-sync CLI / build-time | getServiceToken, onSyncError |

Wiring (the five files + env)

lib/auth.js — NextAuth options

The Keycloak provider instance stays here, on your side — never let it be bundled by a dependency, or it resolves to undefined at runtime.

import KeycloakProvider from "next-auth/providers/keycloak";
import { createCmsAuthOptions } from "@skylab-kulubu/inscribed-auth/server";

export const authOptions = createCmsAuthOptions({
  provider: KeycloakProvider({
    clientId: process.env.KEYCLOAK_CLIENT_ID ?? "",
    clientSecret: process.env.KEYCLOAK_CLIENT_SECRET ?? "",
    issuer: process.env.KEYCLOAK_ISSUER ?? "",
  }),
  adminRole: "cms:access", // Keycloak client role gating admin access (default)
});

By default createCmsAuthOptions also:

  • sets pages.signIn to /api/signin so every sign-in redirect (including the silent re-auth after a token refresh fails) jumps straight into Keycloak instead of NextAuth's "Sign in with X" picker. Mount that route (next file) for it to work, or pass signInPage: false to keep the built-in picker.
  • ends the Keycloak SSO session on sign-out (RP-initiated logout via id_token_hint), so signing out of the app also signs the user out of Keycloak — otherwise the next visit silently re-authenticates against the still-live SSO session.

app/api/auth/[...nextauth]/route.js — NextAuth handler

import NextAuth from "next-auth";
import { authOptions } from "../../../../lib/auth.js";

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

app/api/signin/route.js — one-click sign-in

/api/signin?callbackUrl=... jumps straight into the Keycloak flow, skipping NextAuth's provider-picker page. createCmsAuthOptions wires this as the default pages.signIn, so NextAuth sends unauthenticated users here automatically; you can also link to it from anywhere (<a href="/api/signin">). Mount it — with signInPage defaulting to /api/signin, sign-in 404s if this route is missing (or set signInPage: false).

export { GET } from "@skylab-kulubu/inscribed-auth/signin";

The page shows an animated Skylab loader and, if sign-in hasn't completed after 10 s (slow network, or a strict CSP that blocks the inline script), reveals a "continue to sign in" link so the user is never stuck. Its theming auto-adapts to light/dark (system canvas in light, a warm near-black #1c1815 in dark). Being a standalone document it can't read your app's CSS, so pass concrete values to match:

import { createSignInRoute } from "@skylab-kulubu/inscribed-auth/signin";

// background/color accept any CSS value (hex, rgb, gradient); the loader and
// text are drawn in `color`.
export const GET = createSignInRoute({ background: "#0b1020", color: "#e5e7eb" });

lib/cms.jsx — the CMS page factory

import { revalidateCmsSlug } from "inscribed/actions";
import { createCmsPage } from "inscribed/page";
import { NextAuthCmsProvider } from "@skylab-kulubu/inscribed-auth";
import { withCmsAuth, getClientCredentialsToken } from "@skylab-kulubu/inscribed-auth/server";

import { authOptions } from "./auth.js";

export const CmsPage = createCmsPage({
  Provider: NextAuthCmsProvider,
  config: {
    baseUrl: process.env.CMS_URL ?? "http://localhost:5000",
    cdnUrl: process.env.CMS_CDN_URL,
  },
  // Service token for the public SSR content fetch (no session required).
  getServiceToken: getClientCredentialsToken,
  // Adapts NextAuth into inscribed's auth-agnostic callbacks
  // (getSession / deriveAdmin / deriveUserSub).
  ...withCmsAuth(authOptions),
  // Must come through inscribed's `actions` entry so its "use server"
  // directive survives bundling.
  onAfterSave: revalidateCmsSlug,
});

cms.config.mjscms-sync CLI wiring (project root, .mjs)

The CLI is plain Node and import()s this file. One line:

export { getServiceToken, onSyncError } from "@skylab-kulubu/inscribed-auth/config";

.env.local

# Keycloak (same client serves login + client_credentials sync)
KEYCLOAK_CLIENT_ID=<your-client-id>
KEYCLOAK_CLIENT_SECRET=<your-client-secret>
KEYCLOAK_ISSUER=https://<keycloak-host>/realms/<realm>

# NextAuth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=<run: openssl rand -base64 32>

# CMS backend
CMS_URL=https://<your-cms-host>
# Optional:
# CMS_CDN_URL=https://<your-cdn-host>

| Var | Required | Purpose | | --- | --- | --- | | KEYCLOAK_CLIENT_ID | ✅ | Keycloak client (both grants) | | KEYCLOAK_CLIENT_SECRET | ✅ | Client secret | | KEYCLOAK_ISSUER | ✅ | Realm issuer URL | | NEXTAUTH_URL | ✅ | App base URL for NextAuth | | NEXTAUTH_SECRET | ✅ | NextAuth JWT/session secret | | CMS_URL | ✅ | inscribed backend base URL (→ config.baseUrl) | | CMS_CDN_URL | — | Asset CDN base (→ config.cdnUrl) |

Session shape

createCmsAuthOptions augments the NextAuth session with these fields (on top of NextAuth's defaults):

const session = await getServerSession(authOptions); // server
// or useSession() on the client

session.accessToken;        // string — the raw Keycloak access token
session.user.id;            // string — Keycloak subject (`sub`)
session.user.clientRoles;   // string[] — roles aggregated across every client
                            //   in `resource_access` (includes `cms:access`)
session.user.realmRoles;    // string[] — `realm_access.roles` (realm-wide roles)

Both role arrays are extracted from the access token on sign-in and re-extracted on every silent refresh, and default to [] (never undefined). Use clientRoles for resource-server permissions like cms:access (see below) and realmRoles for realm-wide roles. For admin gating specifically, prefer isCmsAdmin(session, meta) / withCmsAuth over checking the arrays by hand.

Admin access

Admin operations require the cms:access Keycloak client role, both for the logged-in user (admin UI) and the service account (sync). The role belongs to the inscribed backend's Keycloak client (the resource server, e.g. skycms) — not the frontend login client (KEYCLOAK_CLIENT_ID). As long as the backend client is mapped into the token audience, the role rides along under resource_access["<backend-client>"]; the SDK reads roles from every client in resource_access, so it doesn't matter that the role isn't keyed under the frontend client / token azp.

Grant the backend client's cms:access role to each principal in Keycloak Admin:

  • Admin users → Users → <user> → Role mapping → assign cms:access
  • Service account (sync) → Clients → KEYCLOAK_CLIENT_ID → Service account roles → assign cms:access

If sync still returns 403, onSyncError dumps the service token's azp / aud / resource_access claims and reports which client (if any) actually holds cms:access.

License

MIT © Skylab Kulübü — see LICENSE.

Uses inscribed (LGPL-3.0-or-later) as a peer dependency (not bundled); that license governs inscribed itself, not this adapter.