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

@dehwyyy/auth

v2.0.0

Published

dehwyyy auth utilities

Downloads

249

Readme

@dehwyyy/auth

Browser-side OIDC auth client for the Paylonium SPAs. Talks directly to Zitadel using Authorization Code + PKCE (S256). No backend auth proxy.

  • ESM only, single runtime dependency: openapi-fetch.
  • PKCE via WebCrypto (no extra deps).
  • Tokens in localStorage (accessToken / refreshToken / idToken).
  • PKCE verifier / state / returnTo in sessionStorage.
  • Refresh-token rotation with single-flight and cross-tab compare-and-clear.
  • Vue Router guard with role checks and an OIDC-callback anti-loop.

Install

bun add @dehwyyy/auth

Both @dehwyyy/auth and @dehwyyy/auth/v2 resolve to the same v2 entrypoint.

Configuration

import type { ZitadelConfig } from "@dehwyyy/auth"

const config: ZitadelConfig = {
  issuer: "https://core.auth.my-paylonium.com",
  clientId: "376852297126373720",
  redirectUri: window.location.origin + "/auth/callback",
  postLogoutRedirectUri: window.location.origin + "/",
  // scopes is optional; this is the default:
  // ["openid","profile","email","offline_access",
  //  "urn:zitadel:iam:org:projects:roles","urn:zitadel:iam:user:metadata"]
}

Wiring with createAuth

createAuth is the single wiring entry point. It builds the refresh client, the authenticated API client (access-token attach + auto-refresh on 401), the services, and a MiddlewareBuilder you can reuse for your own API clients.

import { createAuth } from "@dehwyyy/auth"

export const auth = createAuth(config)
// auth.authService      — Login / HandleCallback / GetMe / Logout
// auth.baseAuthService  — Refresh / RequestWithAccessToken
// auth.middlewareBuilder — for wiring your own openapi-fetch clients
// auth.callbackPath     — derived from config.redirectUri (e.g. "/auth/callback")

Reuse the middleware for your domain API client:

import createClient from "openapi-fetch"
import type { paths } from "./api/schema"

export const api = createClient<paths>({ baseUrl: import.meta.env.VITE_API_URL })
api.use(...auth.middlewareBuilder.buildDefault())

Login

// returnTo is optional; pass the path you want to land on after auth.
await auth.authService.Login(router.currentRoute.value.fullPath)
// redirects the browser to Zitadel's /oauth/v2/authorize

Callback page

Mount this on config.redirectUri (e.g. /auth/callback). It performs the token exchange, fetches the user, and redirects to returnTo.

<script setup lang="ts">
import { onMounted } from "vue"
import { useRouter } from "vue-router"
import { CallbackError } from "@dehwyyy/auth"
import { auth } from "@/auth"

const router = useRouter()

onMounted(async () => {
  try {
    const { returnTo } = await auth.authService.HandleCallback()
    // returnTo is already same-origin/relative-validated by the library
    // (a foreign origin is dropped to null), so it is safe to navigate to.
    await router.replace(returnTo ?? "/")
  } catch (e) {
    if (e instanceof CallbackError) {
      // e.code: "state_mismatch" | "verifier_missing" | "code_missing" | "exchange_failed"
      console.error("auth callback failed:", e.code)
    }
    await router.replace("/")
  }
})
</script>

<template>
  <div>Signing you in…</div>
</template>

HandleCallback is idempotent per authorization code: a double mount (Vue dev StrictMode) or an F5 on the callback URL reuses the in-flight/resolved exchange instead of failing with a state mismatch.

Route guard

import { RouterAuthGuard } from "@dehwyyy/auth"
import { auth } from "@/auth"

const guard = new RouterAuthGuard(auth.authService, {
  callbackPath: auth.callbackPath,         // required — only this route may carry ?code&state
  fallbackPage: "/forbidden",
  baseServiceURL: window.location.origin,
  onGetMe: (user) => { /* hydrate your store */ },
  // cacheTTL is optional, defaults to 15000 (ms)
})

router.beforeEach(guard.Authorized(["ADMIN", "DEV"]))   // require one of these roles
// or guard.Authorized() to require only authentication
// or guard.Unauthorized([{ to: "/dashboard", roles: ["MERCHANT"] }]) on public pages

Security note: the guard only treats a navigation as the OIDC callback when the target path equals callbackPath and code+state are present. Appending ?code&state to any other protected route does not bypass the guard.

Logout

await auth.authService.Logout()
// clears tokens and redirects to Zitadel /oidc/v1/end_session

Composability (without createAuth)

The pieces remain individually exported for custom wiring: AuthService, BaseAuthService, ClientBuilder, ClientInstance, MiddlewareBuilder, CreateAuthClientInstance, BaseAuthClientInstance, RouterAuthGuard, CallbackError, plus types ZitadelConfig, GetMeResponse, VerboseUserInfo, HandleCallbackResult, RouteLocation, CallbackErrorCode, Auth.

Footgun: BaseAuthClientInstance is a process-wide singleton

BaseAuthClientInstance(issuer) caches its client on first call — the first issuer wins for the module's lifetime, and later calls with a different issuer return the original client. For a single-issuer SPA this is fine. If you need multiple issuers (or isolated instances in tests), use createAuth instead, which constructs fresh clients each call.

GetMe response shape

type GetMeResponse = {
  user_id: string
  roles: string[]    // keys of the Zitadel project-roles claim
  active: boolean     // always true when a userinfo response is returned
  info?: {            // populated only when verbose
    username?: string
    email?: string
    email_verified?: boolean
    name?: string
    picture?: string
  }
}

Development

bun install
bun run gen     # regenerate src/v2/pkg/types/schema.d.ts from spec/zitadel-oidc.yaml
bun run build   # tsc + tsc-alias (full-path ESM resolution)
bun test

The OIDC surface (/oauth/v2/token, /oidc/v1/userinfo, /oidc/v1/end_session) is described in spec/zitadel-oidc.yaml; all schema types are generated from it via openapi-typescript. Do not hand-edit schema.d.ts.