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

@blackforestsentinel/seed-web-auth

v0.8.0

Published

Anmeldung per MSAL für Frontends in Sentinel-Seed-Projekten: Login mit stiller Erneuerung, Access-Tokens für die Function-API und Capabilities der angemeldeten Person für das UI; dazu ein Einstieg für Browser-Erweiterungen.

Readme

@blackforestsentinel/seed-web-auth

Anmeldung per MSAL für Frontends in Projekten aus Sentinel Seed. Die Werte kommen aus der Laufzeitkonfiguration, die das Terraform-Modul sso über die Pipeline in config.json schreibt.

import { createHttpClient, loadRuntimeConfig } from '@blackforestsentinel/seed-web-core';
import { createSeedAuth, isSeedAuthConfig } from '@blackforestsentinel/seed-web-auth';

const config = await loadRuntimeConfig<{ apiBaseUrl: string; auth?: unknown }>();
if (isSeedAuthConfig(config.auth)) {
  const auth = await createSeedAuth(config.auth);
  if (!auth.account) await auth.login();
  const api = createHttpClient({ baseUrl: config.apiBaseUrl, getAccessToken: auth.getAccessToken });
}
  • createSeedAuth() schließt eine laufende Anmeldung per Redirect ab; danach ist account gesetzt.
  • getAccessToken() holt Tokens still und leitet nur um, wenn Entra ID eine Interaktion verlangt. Dann wirft es SeedAuthRedirectError.
  • getAccessToken(scopes) holt ein Token für genau diese Berechtigungen, etwa für Microsoft Graph (unten).
  • Redirect-Ziel ist der Origin der Seite mit abschließendem Schrägstrich (https://app.example.org/). Das Terraform-Modul sso trägt genau diese Form für die Static Web App und in dev für http://localhost:5173/ ein.
  • Browser-Erweiterungen nutzen statt createSeedAuth() den Einstieg /extension mit demselben Interface (unten, „Browser-Erweiterungen“).

Stille Sitzungserneuerung

Entra ID begrenzt das Refresh-Token einer SPA auf 24 Stunden. Danach erneuert seed-web-auth die Sitzung in dieser Reihenfolge und geht erst zum nächsten Schritt, wenn der vorige scheitert:

  1. Access-Token aus dem Cache oder per Refresh-Token, ohne iframe.
  2. ssoSilent mit dem Benutzernamen als loginHint in einem unsichtbaren iframe. Das nutzt die Sitzung bei Entra ID statt des abgelaufenen Refresh-Tokens, braucht aber die Bridge-Seite (unten) und Cookies von Drittanbietern.
  3. Umleitung zu Entra ID mit loginHint und prompt=none: Mit Sitzung kommt die Seite sofort zurück, ohne Sitzung mit einem Fehler statt eines Anmeldedialogs. Erst der nächste Versuch im selben Tab ist interaktiv.

Netzwerkfehler leiten nicht um, alle anderen Fehler beim stillen Erneuern schon; auch timed_out und redirect_bridge_timeout, die MSAL statt eines InteractionRequiredAuthError meldet. Parallele Aufrufe teilen sich eine Erneuerung. Der Benutzername liegt im localStorage, damit auch ein neuer Tab still anmelden kann; logout() löscht ihn. Wer noch nie angemeldet war, wird direkt interaktiv umgeleitet.

Bridge-Seite

MSAL 5 lädt die Redirect-URI der stillen Anmeldung im iframe und erwartet dort eine eigene Seite, die die Antwort per broadcastResponseToMainFrame an die App weiterreicht. Die Startseite der App kann das nicht. Ohne Bridge-Seite überspringt seed-web-auth den Schritt ssoSilent und leitet direkt um.

<!-- frontend/redirect.html, in vite.config.ts als zweiter Einstieg -->
<script type="module" src="/src/redirect.ts"></script>
// frontend/src/redirect.ts
import { handleSeedAuthRedirect } from '@blackforestsentinel/seed-web-auth/bridge';
void handleSeedAuthRedirect();

Die Redirect-URI <origin>/redirect.html trägt das Modul sso mit spa_redirect_bridge_path = "/redirect.html" ein und schreibt redirectBridgePath in config.json.

Tokens für Microsoft Graph

getAccessToken() ohne Argument liefert das Token für die API. Mit einer Liste von Scopes liefert es eines für genau diese, etwa für Graph:

const token = await auth.getAccessToken(['https://graph.microsoft.com/User.ReadBasic.All']);
  • Dieselbe Reihenfolge wie für die API: Cache und Refresh-Token, dann ssoSilent, dann Umleitung.
  • Parallele Aufrufe je Scope-Satz teilen sich eine Anfrage; die Reihenfolge der Scopes spielt keine Rolle.
  • Fehlt die Einwilligung, leitet es je Tab und Scope-Satz höchstens zweimal um: erst ohne Dialog, dann mit, damit die Person zustimmen kann. Danach wirft es SeedAuthScopeError statt erneut umzuleiten, und die App läuft ohne diese Berechtigungen weiter. Kommt die Umleitung mit access_denied zurück (Einwilligung abgelehnt oder nicht erlaubt), startet die App trotzdem.
  • Ein Token für die API setzt den Merker für Graph nicht zurück, logout() schon.

@blackforestsentinel/seed-web-people holt so Fotos und Profile. Welche Graph-Berechtigungen das Frontend anfragt, steht in config.json unter auth.directory (Typ SeedDirectoryConfig); das Terraform-Modul sso schreibt den Teil, wenn project.yaml das Verzeichnis einschaltet:

"auth": {
  "clientId": "…", "tenantId": "…", "apiScope": "…", "redirectBridgePath": "/redirect.html",
  "directory": {
    "scopes": ["https://graph.microsoft.com/User.Read", "https://graph.microsoft.com/User.ReadBasic.All", "https://graph.microsoft.com/People.Read", "https://graph.microsoft.com/Presence.Read.All"],
    "presence": true,
    "roleFilter": false,
    "apiClientId": "<Client-ID der API-Registrierung>"
  }
}

Im lokalen Modus gibt es keinen Graph-Zugriff; auth.directory.people enthält dort erfundene Personen (Typ SeedLocalDirectoryConfig).

Browser-Erweiterungen

Eine Erweiterung (Edge, Chrome, Manifest V3) läuft unter chrome-extension://<id> statt auf einer Web-Origin. Umleitung der ganzen Seite, Token-Cache je Tab und Bridge-Seite passen dort nicht. Dafür gibt es den Einstieg /extension: createSeedExtensionAuth() liefert dasselbe Interface SeedAuth wie createSeedAuth(), damit laufen createHttpClient, SeedUserProvider und die Komponenten aus @blackforestsentinel/seed-web-people unverändert. Er meldet per Autorisierungscode mit PKCE über chrome.identity.launchWebAuthFlow an und lädt MSAL nicht (rund 3 KB gzip im Bundle).

import { createHttpClient, loadRuntimeConfig } from '@blackforestsentinel/seed-web-core';
import { createSeedExtensionAuth, isSeedAuthConfig, SeedAuthInteractionRequiredError } from '@blackforestsentinel/seed-web-auth/extension';

const config = await loadRuntimeConfig<{ apiBaseUrl: string; auth?: unknown }>({ url: chrome.runtime.getURL('config.json') });
if (isSeedAuthConfig(config.auth)) {
  const auth = await createSeedExtensionAuth(config.auth, { domainHint: 'example.org' });
  const api = createHttpClient({ baseUrl: config.apiBaseUrl, getAccessToken: auth.getAccessToken });
  try {
    render(await api.get('/api/items'));
  } catch (error) {
    if (error instanceof SeedAuthInteractionRequiredError) {
      // „Anmeldung erneuern“ anzeigen und erst nach dem Klick auth.login() aufrufen.
    }
  }
}

getAccessToken(scopes?) geht diese Stufen durch und nimmt die nächste nur, wenn die vorige scheitert:

  1. Access-Token aus dem Speicher oder per Refresh-Token.
  2. launchWebAuthFlow ohne Fenster (interactive: false) mit prompt=none und dem Benutzernamen als login_hint. Das nutzt die Sitzung bei Entra ID im Browser-Profil, etwa nach den 24 Stunden, die ein Refresh-Token einer SPA gilt, oder nach einem Browser-Neustart.
  3. Kein Fenster: SeedAuthInteractionRequiredError. Für zusätzliche Berechtigungen (etwa Graph) gilt wie im Web SeedAuthScopeError, wenn nur die Einwilligung fehlt.
  • Nur login() öffnet ein Fenster, mit domain_hint und, falls bekannt, login_hint. Die App ruft es nur nach einem Klick auf. Nirgends leitet der Einstieg die Seite um.
  • account kommt beim Start sofort aus chrome.storage.local, ohne Netz und ohne Token; der neue Tab kann also gleich den Namen zeigen. Dort liegt auch der Benutzername für login_hint, damit der Service Worker ihn kennt.
  • Die Tokens gelten für alle Seiten und den Service Worker der Erweiterung gemeinsam. tokenStorage: 'session' (Default) legt sie in chrome.storage.session: nur im Speicher, nach einem Neustart meldet Stufe 2 still neu an. tokenStorage: 'local' legt sie in chrome.storage.local, wo sie Neustarts überdauern, solange das Refresh-Token gilt (Warnung unten). Beim Start löscht createSeedExtensionAuth() die Tokens aus dem jeweils anderen Bereich; nach einem Wechsel zurück auf 'session' bleibt kein Refresh-Token liegen.
  • Parallele Aufrufe teilen sich eine Erneuerung, auch über Tabs hinweg (navigator.locks).
  • Scheitert Stufe 2, versucht der Einstieg sie für denselben Scope-Satz in dieser Browser-Sitzung nicht noch einmal, sondern wirft sofort; login() setzt das zurück. Netzwerk- und Serverfehler des Token-Endpunkts lösen Stufe 2 nicht aus, sondern gehen weiter.
  • logout() löscht Tokens, Konto und Merker. Die Sitzung bei Entra ID gehört dem Browser-Profil und bleibt.
  • redirectBridgePath aus der Konfiguration gilt hier nicht; sonst ist sie dieselbe wie im Web, auch directory für Graph.

Warnung zu tokenStorage: 'local': Das Refresh-Token liegt dann unverschlüsselt im Browser-Profil auf der Platte und ist für die Content Scripts der Erweiterung lesbar, die in fremden Webseiten laufen; eine Lücke dort reicht, um es abzugreifen. Damit lassen sich bis zu 24 Stunden lang Tokens für die API holen, mit Einwilligung auch für Graph. Beim Default 'session' bleiben, Stufe 2 überbrückt Neustarts ohne Fenster. Den Zugriff auf chrome.storage.session nie per chrome.storage.session.setAccessLevel() für Content Scripts öffnen (TRUSTED_AND_UNTRUSTED_CONTEXTS); dann gälte dort dasselbe.

Einrichtung:

  • manifest.json braucht "permissions": ["identity", "storage"]. host_permissions für Entra ID oder die API sind nicht nötig. Mit key im Manifest behält eine entpackte Erweiterung ihre ID; die Store-Version bekommt ihre ID vom Store.
  • Die Redirect-URI ist chrome.identity.getRedirectURL(), also https://<erweiterungs-id>.chromiumapp.org/. Sie gehört an die SPA-Registrierung, damit der Token-Endpunkt die Einlösung mit Origin: chrome-extension://<id> annimmt; im Template je Umgebung über auth.extraRedirectUris in project.yaml. Die API muss die Origin chrome-extension://<id> per CORS zulassen (hosting.corsAllowedOrigins). Beides beschreibt die README von seed-template, Abschnitt „Browser-Erweiterungen“.
  • config.json liegt im Paket der Erweiterung; die Pipeline aus seed-pipelines schreibt sie je Umgebung (Parameter extension).

Capabilities im UI

Die API liefert unter GET /api/me die angemeldete Person samt Capabilities, aufgelöst aus ihren App-Rollen (SeedUserInfo aus Bfs.Seed.Auth). Das Frontend kennt die Zuordnung nicht, es blendet nur ein oder aus. Durchgesetzt werden die Rechte in der API per [RequireCapability].

import { IfCapability, SeedUserProvider, loadSeedUser, useCapabilities } from '@blackforestsentinel/seed-web-auth/react';

<SeedUserProvider load={() => loadSeedUser(http)}>
  <IfCapability capability="invoices.write" fallback={<p>Nur lesen</p>}>
    <button>Rechnung anlegen</button>
  </IfCapability>
</SeedUserProvider>

const canWrite = useCapabilities().has('invoices.write');
  • SeedUserProvider lädt die Person einmal beim Einhängen; solange sie lädt, ohne Anmeldung oder bei einem Fehler sind keine Capabilities gesetzt und geschützte Elemente bleiben aus.
  • useSeedUser() liefert { status, user } mit status loading, ready oder error.
  • user.roleDetails nennt die Anzeigenamen der Rollen aus project.yaml ({ value: 'Admin', displayName: 'Administrator' }), wenn die API sie liefert; roleDisplayNames(user) gibt sie in der Reihenfolge von roles zurück, ohne Anzeigenamen den Wert.
  • Ohne React: loadSeedUser(http) und hasCapability(user, 'invoices.write') aus dem Haupteinstieg.

Der Einstieg /react braucht React ab Version 18 und lädt MSAL nicht.

Lokaler Modus

Steht in config.json "auth": { "mode": "local" }, startet das Frontend ohne MSAL (isSeedLocalAuthConfig()); die API läuft dann mit Auth:Mode = Local und liefert unter /api/me den Entwicklungsnutzer mit seinen Capabilities.