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

@all-or-nothing-hc/impersonate

v0.2.2

Published

Generic Supabase admin "login as user" (impersonation) — Edge Function + React hook + banner component.

Downloads

78

Readme

impersonate

Generisches Supabase "Admin-Login-als-User" (Assume Identity / Impersonation) — eine Edge Function + ein React-Hook + eine Banner-Komponente. Für mehrere Projekte gedacht (Vocab, Taskly, Trainingsapps) statt in ein einzelnes Projekt fest verdrahtet.

Status: läuft produktiv in Taskly (Start+Stop-Roundtrip bestätigt, 2026-09-03). Dieses Repo selbst ist gerade im Umbau von reiner Datei-Kopie zu echtem npm-Package (@all-or-nothing-hc/impersonate, öffentlich auf npm) — Taskly nutzt aktuell noch eine lokale Kopie von Client- und Server-Code, siehe "Distribution" unten. Ein Logik-Review- Spike (2026-08-26) hat mehrere Bugs gefunden und behoben (siehe unten unter "Sicherheitsannahmen").

Wie es funktioniert

Kein Token/keine Session wird jemals clientseitig zwischengespeichert. Das einzige, was lokal (in localStorage) liegt, ist eine nicht-geheime Referenz — die id der Audit-Log-Zeile (auditLogId). Start und Stop sind beide symmetrische Session-Mint-Roundtrips über den Server; die Datenbank ist die alleinige Quelle der Wahrheit dafür, wer gerade wen impersoniert.

  1. Admin ruft useImpersonation().startImpersonation(targetUserId) auf.
  2. Der Hook ruft die Edge Function (action: 'start') mit dem eigenen Admin-Token auf.
  3. Die Function prüft serverseitig: ist der Aufrufer Admin? Ist das Ziel im gleichen Scope (Standard: family_id)? Ist das Ziel selbst kein Admin? Erst dann wird eine Session für das Ziel gemintet und ein Audit-Log- Eintrag geschrieben (dessen id an den Client zurückgeht).
  4. Der Client übernimmt die zurückgegebene Session direkt (supabase.auth.setSession({ access_token, refresh_token })) — ab hier läuft die App exakt wie für den echten Ziel-User, mit allen echten Konsequenzen. Lokal wird nur { auditLogId, email, startedAt } in localStorage abgelegt (Key impersonate:active), zusammen mit einem Auto-Stop-Timer (maxDurationMs, Default 30 Minuten), der auch über einen Reload hinweg korrekt weiterläuft (berechnet aus startedAt).
  5. Weil localStorage (nicht sessionStorage) verwendet wird, sieht ein neuer Tab im selben Browser die aktive Impersonation sofort — ein storage-Event-Listener hält alle offenen Tabs synchron (Banner erscheint/verschwindet überall gleichzeitig, Timer wird überall neu geplant).
  6. stopImpersonation() ruft die Function (action: 'end') mit der aktuell aktiven Session (also der des Ziel-Users) und der gestashten auditLogId auf. Die Function verifiziert, dass der Aufrufer wirklich der impersonierte Ziel-User ist, setzt ended_at im Audit-Log (best effort) und mintet eine frische Session für den ursprünglichen Admin. Der Client übernimmt auch diese Session ganz normal über setSession() — kein Unterschied zum Start-Vorgang, nur in umgekehrter Richtung. Schlägt irgendein Schritt fehl, wird niemals stillschweigend die Ziel-User-Session belassen: als Fallback greift supabase.auth.signOut().

Session-Minting ist pluggable

Wie die Function eine Session tatsächlich mintet, ist server-seitig über sessionMint konfigurierbar (ImpersonateHandlerConfig in src/server/index.ts) — der Client sieht in beiden Fällen dieselbe { accessToken, refreshToken }-Antwort und muss die Strategie nicht kennen:

  • { strategy: 'generate-link' } (Standard). Der normale Weg für Supabase Cloud und jedes gesunde self-hosted GoTrue: admin.generateLink() erzeugt einen Magic-Link-Token, der serverseitig sofort per verifyOtp() gegen eine echte Session eingelöst wird.
  • { strategy: 'direct-db', jwtSecret, dbUrl, sessionTtlSeconds? } (optional). Workaround für self-hosted GoTrue-Instanzen, die den Upstream-Bug supabase/auth#2057 (Regression aus v2.175.0) treffen — dort schlägt admin.generateLink() für JEDEN existierenden User mit duplicate key value violates unique constraint "users_email_partial_key" fehl. Diese Strategie signiert den Access-Token selbst (HS256, gleicher Secret wie GoTrue — GOTRUE_JWT_SECRET/PGRST_JWT_SECRET) und schreibt Session + Refresh-Token direkt per Postgres-Verbindung in auth.sessions/auth.refresh_tokens. Das Paket postgres ist eine reguläre Dependency (nicht optional) — läuft beim Auflösen von npm:@all-or-nothing-hc/impersonate/server automatisch mit, ohne dass Projekte, die diese Strategie gar nicht nutzen, dadurch nennenswert mehr installieren. (War in 0.1.x/0.2.0 fälschlich eine optionale Peer- Dependency — Denos npm-Resolver löst optionale Peer-Deps nicht auf, solange nichts im Import-Graph sie referenziert, was direct-db beim ersten echten Einsatz kaputt gemacht hat. Seit 0.2.1 gefixt.) Supabase- Cloud-Projekte treffen diesen Bug vermutlich nicht und sollten beim Standardpfad bleiben.

Setup pro Projekt

1. Edge Function deployen

supabase functions deploy impersonate --project-ref <dein-projekt-ref>

Falls eure Tabellen-/Spaltennamen von den Defaults abweichen, oder um die CORS-Origin einzuschränken (dringend empfohlen vor Produktiveinsatz — Default ist *), als Secrets setzen:

supabase secrets set \
  IMPERSONATE_ROLES_TABLE=user_roles \
  IMPERSONATE_ADMIN_COLUMN=is_admin \
  IMPERSONATE_SCOPE_COLUMN=family_id \
  IMPERSONATE_USER_ID_COLUMN=user_id \
  IMPERSONATE_AUDIT_TABLE=impersonation_log \
  IMPERSONATE_ALLOWED_ORIGIN=https://eure-app.example.com

Nur nötig auf self-hosted GoTrue-Instanzen, die den generateLink-Bug (supabase/auth#2057) treffen — sonst weglassen, Default bleibt generate-link:

supabase secrets set \
  IMPERSONATE_SESSION_STRATEGY=direct-db \
  JWT_SECRET=<gleicher-wert-wie-GOTRUE_JWT_SECRET> \
  SUPABASE_DB_URL=<direkte-postgres-connection-string> \
  IMPERSONATE_SESSION_TTL_SECONDS=3600

postgres müsst ihr dafür nicht separat einrichten — ist eine reguläre Dependency des Pakets, läuft beim Import über npm:@all-or-nothing-hc/impersonate/server automatisch mit.

Nicht-boolesche Admin-Spalte (z.B. eine role-Textspalte statt eines booleschen is_admin):

supabase secrets set \
  IMPERSONATE_ADMIN_COLUMN=role \
  IMPERSONATE_ADMIN_VALUE=admin

IMPERSONATE_ADMIN_VALUE ist der Wert, der adminColumn mit === vergleicht (Default: true, passend für eine boolesche Spalte).

Kein Scope-Konzept (z.B. genau eine globale Admin-Ebene ohne family_id/org_id-Äquivalent — Vorsicht, das ist eine echte Autorisierungs-Entscheidung, kein Config-Detail, siehe Kommentar bei scopeColumn in src/server/index.ts):

supabase secrets set IMPERSONATE_SCOPE_COLUMN=

Ein explizit leerer Wert deaktiviert den Scope-Check komplett — dann darf jeder Admin jeden Nicht-Admin impersonieren. Env-Var einfach weglassen lässt den Default (family_id) aktiv.

2. Migration übernehmen

supabase/migrations/0001_impersonation_log.sql in die eigene Migrations-Historie des Projekts kopieren/anpassen und ausführen.

3. Paket installieren

npm install @all-or-nothing-hc/impersonate

4. Im Frontend verwenden

import { useImpersonation, ImpersonationBar } from '@all-or-nothing-hc/impersonate'
import { supabase } from './lib/supabase'

function AdminShell() {
  // functionName defaults to 'impersonate' — only pass it if you deployed
  // under a different name. The hook calls supabase.functions.invoke()
  // under the hood, so it inherits the URL/apikey already configured on
  // your `supabase` client — no separate function URL to build by hand.
  const impersonation = useImpersonation({ supabase })

  return (
    <>
      <ImpersonationBar
        isImpersonating={impersonation.isImpersonating}
        impersonatingEmail={impersonation.impersonatingEmail}
        onStop={impersonation.stopImpersonation}
      />
      {/* ... restliche App ... */}
    </>
  )
}

5. Eigene Edge Function statt supabase functions deploy aus diesem Repo

Wer die Function lieber im eigenen Projekt-Repo deployt (z.B. weil dort schon eine CI dafür existiert), kann sie als dünnen Wrapper um das ./server-Subpath des Pakets bauen, statt Schritt 1 aus diesem Repo zu nutzen:

// supabase/functions/impersonate/index.ts im eigenen Projekt
import { createImpersonateHandler, impersonateConfigFromEnv } from 'npm:@all-or-nothing-hc/impersonate/server'

Deno.serve(createImpersonateHandler(impersonateConfigFromEnv()))

impersonateConfigFromEnv() liest dieselben IMPERSONATE_*/SUPABASE_*/ JWT_SECRET-Secrets wie oben. Alternativ createImpersonateHandler({...}) direkt mit einem Objekt aufrufen statt über Env-Vars zu konfigurieren.

Sicherheitsannahmen (nicht verhandelbar, siehe Edge Function)

  • Aufrufer muss is_admin === true sein.
  • Ziel-User muss im gleichen Scope sein (Spalte konfigurierbar).
  • Ziel-User darf selbst kein Admin sein (Privilege-Escalation-Schutz).
  • Jeder Start-Aufruf wird auditiert (impersonation_log), inklusive ended_at.
  • maxDurationMs (Default 30 Min) stoppt eine vergessene, offene Session automatisch — aber nur clientseitig, solange der Tab offen bleibt. Das ist keine echte Sicherheitskontrolle, nur ein Sicherheitsnetz gegen "Tab offen vergessen".
  • Es werden keine Auth-Tokens oder Session-Objekte im Browser zwischengespeichert — weder für den Ziel-User noch für den Admin. Die einzige lokal gehaltene Information ist eine nicht-geheime UUID (auditLogId), die ohne den serverseitigen DB-Lookup + Live-Auth-Check wertlos ist. Wichtig dabei (Fix 2026-09-03, siehe Deep-Dive-Review): action: 'end' prüft zusätzlich, dass die Session noch nicht beendet ist (ended_at == null) — ohne diesen Check hätte jeder, der eine alte auditLogId UND ein gültiges JWT des damaligen Ziel-Users kennt, beliebig oft eine frische Admin-Session nachmagen können, auch lange nach dem eigentlichen Ende, und ohne dass das im Audit-Log auftaucht (ein Replay liest nur die bestehende Zeile, schreibt keine neue). Genau dieser Check macht die "nicht-geheim, weil wertlos ohne Server-Lookup"-Aussage oben erst wahr.
  • Ein Start ohne funktionierenden Audit-Log-Eintrag ist unmöglich (fail-closed): der Audit-Insert passiert, bevor irgendein Token gemintet wird; schlägt er fehl, wird der Start abgebrochen. Schlägt danach das Minten fehl, wird die Audit-Zeile wieder best-effort gelöscht (kein Datenmüll für nie gewährte Sessions). Das Schließen (ended_at) am Ende bleibt bewusst best-effort — die Session ist zu diesem Zeitpunkt schon auditiert, nur der Abschluss-Zeitstempel könnte fehlen, und den Admin deswegen als Ziel-User stranden zu lassen wäre schlimmer.
  • Alle Autorisierungs-Ablehnungen (Aufrufer kein Admin, Ziel nicht gefunden, Ziel ist selbst Admin, Ziel außerhalb des Scopes, Aufrufer ist nicht der Session-Ziel-User beim Beenden) liefern nach außen dieselbe generische Meldung ("Not authorized"). Der genaue Grund landet nur im Server-Log — von außen lässt sich nicht mehr per Trial-and-Error herausfinden, ob ein fremder User existiert, Admin ist oder in welchem Scope er liegt.

Design-Entscheidungen aus dem Logik-Review-Spike (2026-08-26) — alle drei erledigt

  • Session-Persistenz über Tab-Grenzen hinweg — entschieden: keine lokalen Sessions mehr, alles über die DB. Siehe "Wie es funktioniert" oben.
  • Audit-Log-Fail-Open — entschieden: fail-closed beim Start (kein Audit-Eintrag → kein Token, kein Start), best-effort/fail-open bleibt nur beim ended_at-Update am Ende (bewusste Asymmetrie, siehe oben unter "Sicherheitsannahmen").
  • Fehlermeldungs-Granularität — entschieden: eine generische Autorisierungs-Meldung für alle Ablehnungsgründe, Details nur im Server-Log.

Distribution

Öffentliches npm-Paket (@all-or-nothing-hc/impersonate, publishConfig.access: "public") — öffentlich, damit Deno-Edge-Functions es ohne Registry-Auth-Token per npm:-Spezifikation direkt importieren können (npm:@all-or-nothing-hc/impersonate/server). Enthält keine Secrets; alle projektspezifischen Werte (Tabellennamen, JWT-Secret, DB-URL) kommen über Config/Env-Vars vom Konsumenten. Zwei Subpaths: . (React-Hook + Banner-Komponente, src/index.ts) und ./server (Edge-Function-Handler, src/server/index.ts).

Konsumierende Projekte werden per Dependabot (.github/dependabot.yml, npm-Ecosystem) auf neue Versionen hingewiesen — bewusst kein Silent-Auto- Update; ein PR mergen ist gleichzeitig die Benachrichtigung, dass aktualisiert wurde. npm publish selbst läuft immer über Carstens eigenen npm-Account, nicht automatisiert.