{"_id":"@all-or-nothing-hc/impersonate","_rev":"5-c75cdc388c06656f80463414a9cb57a0","name":"@all-or-nothing-hc/impersonate","dist-tags":{"latest":"0.2.2"},"versions":{"0.1.0":{"name":"@all-or-nothing-hc/impersonate","version":"0.1.0","license":"UNLICENSED","_id":"@all-or-nothing-hc/impersonate@0.1.0","maintainers":[{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"}],"homepage":"https://github.com/all-or-nothing-hc/impersonate#readme","bugs":{"url":"https://github.com/all-or-nothing-hc/impersonate/issues"},"dist":{"shasum":"3a0a9e75432067fe60a0cb6038806eb786752ace","tarball":"https://registry.npmjs.org/@all-or-nothing-hc/impersonate/-/impersonate-0.1.0.tgz","fileCount":13,"integrity":"sha512-IDx6PwZwlnZEjic6eMPzVfKv5Dl0vD2F1GwpIiZnX6xNNQW9D+4JgmGuYw6+bLfBxaGb/EgJD+O5MHG/CMZSnQ==","signatures":[{"sig":"MEUCIQC1d25MnSOZ1I/qxFRv1vQ28DAlchxZWSSCJOR8OVRzKQIgZIXYYJ/UHyT8beQ5Mm4+5VJlBUk0pLkaPXfkLauyv9M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65537},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"f0e0815409105e36492c8ee40722c26871d50ae3","scripts":{"lint":"eslint src","build":"tsup --entry.index=src/index.ts --entry.server=src/server/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"},"repository":{"url":"git+https://github.com/all-or-nothing-hc/impersonate.git","type":"git"},"_npmVersion":"10.9.4","description":"Generic Supabase admin \"login as user\" (impersonation) — Edge Function + React hook + banner component.","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","postgres":"^3.4.0","typescript":"^5.6.0","@types/react":"^18.3.0","@supabase/supabase-js":"^2.45.0"},"peerDependencies":{"react":">=18","postgres":">=3","@supabase/supabase-js":">=2"},"peerDependenciesMeta":{"react":{"optional":true},"postgres":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/impersonate_0.1.0_1788419662287_0.447962916637791","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@all-or-nothing-hc/impersonate","version":"0.1.1","license":"UNLICENSED","_id":"@all-or-nothing-hc/impersonate@0.1.1","maintainers":[{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"}],"homepage":"https://github.com/all-or-nothing-hc/impersonate#readme","bugs":{"url":"https://github.com/all-or-nothing-hc/impersonate/issues"},"dist":{"shasum":"55ea032218b046e0a5596742de229b41780c8725","tarball":"https://registry.npmjs.org/@all-or-nothing-hc/impersonate/-/impersonate-0.1.1.tgz","fileCount":13,"integrity":"sha512-jeIGvQedgimrDifaSWpSIEvZ2IFxJu38sT+XUJtQeTCH6yenaTMBQapeqVJLAPm1eDvUbw94/ucXzDV85rYXiQ==","signatures":[{"sig":"MEUCIF1BLIPgXADBCXiKw2HZBfbLQ3RTooYry7DVf+R3pbFDAiEAm8QpLz3ZHK3C9f7ywW5wdJeXeMwciA7AEGmByyixrLc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":69205},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"f1ec52019c6e7adfaa8f814e463a0a1580e23886","scripts":{"lint":"eslint src","build":"tsup --entry.index=src/index.ts --entry.server=src/server/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"},"repository":{"url":"git+https://github.com/all-or-nothing-hc/impersonate.git","type":"git"},"_npmVersion":"10.9.4","description":"Generic Supabase admin \"login as user\" (impersonation) — Edge Function + React hook + banner component.","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","postgres":"^3.4.0","typescript":"^5.6.0","@types/react":"^18.3.0","@supabase/supabase-js":"^2.45.0"},"peerDependencies":{"react":">=18","postgres":">=3","@supabase/supabase-js":">=2"},"peerDependenciesMeta":{"react":{"optional":true},"postgres":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/impersonate_0.1.1_1788422623244_0.044130314567698115","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@all-or-nothing-hc/impersonate","version":"0.2.0","license":"UNLICENSED","_id":"@all-or-nothing-hc/impersonate@0.2.0","maintainers":[{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"}],"homepage":"https://github.com/all-or-nothing-hc/impersonate#readme","bugs":{"url":"https://github.com/all-or-nothing-hc/impersonate/issues"},"dist":{"shasum":"e87b946c438e599a6bc1591a754e1101fd375724","tarball":"https://registry.npmjs.org/@all-or-nothing-hc/impersonate/-/impersonate-0.2.0.tgz","fileCount":13,"integrity":"sha512-JC9wMnTB5AX+DkfhKnFMPRhjlh1TyY4kdxiTXdmXlMsP/BQ06yqdpaAWxo/abAiOhqA2H2vVgT/0xYrd+dALTw==","signatures":[{"sig":"MEUCIQC9Gzezb+ENgeuF79llAZcUTkUpYq1ZmkGnOPV2B9VzTwIgYn9i7vdYC22R5stf2ESJjmt/fg0fCmqRsNQU9PrSZvs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":69959},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"8ccc5ccfbf3f7d909ada0d7d19dd8627f5274bff","scripts":{"lint":"eslint src","build":"tsup --entry.index=src/index.ts --entry.server=src/server/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"},"repository":{"url":"git+https://github.com/all-or-nothing-hc/impersonate.git","type":"git"},"_npmVersion":"10.9.4","description":"Generic Supabase admin \"login as user\" (impersonation) — Edge Function + React hook + banner component.","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","postgres":"^3.4.0","typescript":"^5.6.0","@types/react":"^18.3.0","@supabase/supabase-js":"^2.45.0"},"peerDependencies":{"react":">=18","postgres":">=3","@supabase/supabase-js":">=2"},"peerDependenciesMeta":{"react":{"optional":true},"postgres":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/impersonate_0.2.0_1788422982961_0.5329120005444625","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@all-or-nothing-hc/impersonate","version":"0.2.1","license":"UNLICENSED","_id":"@all-or-nothing-hc/impersonate@0.2.1","maintainers":[{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"}],"homepage":"https://github.com/all-or-nothing-hc/impersonate#readme","bugs":{"url":"https://github.com/all-or-nothing-hc/impersonate/issues"},"dist":{"shasum":"71c0a0935b87289187306c079663b821067cd603","tarball":"https://registry.npmjs.org/@all-or-nothing-hc/impersonate/-/impersonate-0.2.1.tgz","fileCount":13,"integrity":"sha512-cztrYlyAq3s9BGWoc/qGzzsoQ9QTl9jq3jx+3lrDOnFdpmvOB5Fex9bQUdnj5nNz1pXiNR5G22YFczdZo40MEw==","signatures":[{"sig":"MEUCIC5Jqp198GJ5ptA141pt2wWJXpR57gzmyR0ew5IrGEJ1AiEAq6q35DiArTjY3YkQQYX4O7u8gspWcSuaNVi8v04cFpU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":70178},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"gitHead":"22d84b542634e5b10d33e039a9548799874b320a","scripts":{"lint":"eslint src","build":"tsup --entry.index=src/index.ts --entry.server=src/server/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"},"repository":{"url":"git+https://github.com/all-or-nothing-hc/impersonate.git","type":"git"},"_npmVersion":"10.9.4","description":"Generic Supabase admin \"login as user\" (impersonation) — Edge Function + React hook + banner component.","directories":{},"_nodeVersion":"22.22.1","dependencies":{"postgres":"^3.4.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","typescript":"^5.6.0","@types/react":"^18.3.0","@supabase/supabase-js":"^2.45.0"},"peerDependencies":{"react":">=18","@supabase/supabase-js":">=2"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/impersonate_0.2.1_1788423767328_0.7631759165008338","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@all-or-nothing-hc/impersonate","version":"0.2.2","description":"Generic Supabase admin \"login as user\" (impersonation) — Edge Function + React hook + banner component.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/server.cjs"}},"scripts":{"build":"tsup --entry.index=src/index.ts --entry.server=src/server/index.ts --format esm,cjs --dts","typecheck":"tsc --noEmit","lint":"eslint src","prepublishOnly":"npm run typecheck && npm run build"},"dependencies":{"postgres":"^3.4.0"},"peerDependencies":{"react":">=18","@supabase/supabase-js":">=2"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@supabase/supabase-js":"^2.45.0","@types/react":"^18.3.0","eslint":"^9.0.0","tsup":"^8.0.0","typescript":"^5.6.0"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/all-or-nothing-hc/impersonate.git"},"license":"UNLICENSED","_id":"@all-or-nothing-hc/impersonate@0.2.2","gitHead":"8c6478e66772b296c988b9ffa02ae041096ee302","bugs":{"url":"https://github.com/all-or-nothing-hc/impersonate/issues"},"homepage":"https://github.com/all-or-nothing-hc/impersonate#readme","_nodeVersion":"22.22.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-5sKZw891b1MZPIBXgc36agwg6vP/YaPdkA69B2ZKdfDoqQG4X/oKYIa/p1bNQa1GAvdhRsgELsreIkF3tpiPeg==","shasum":"c7df19b8f1fda5096ec088dc1e1a9d0b0b85c796","tarball":"https://registry.npmjs.org/@all-or-nothing-hc/impersonate/-/impersonate-0.2.2.tgz","fileCount":13,"unpackedSize":71566,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCvCqSfAGoaVk9GVCe3vHH83R5gYtDowayySiWWi6PgUAIgDkytHZb+vJbCKJ1tOFH866s8WsN2Y9rB64Q5A/BT6hI="}]},"_npmUser":{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"},"directories":{},"maintainers":[{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/impersonate_0.2.2_1788428622061_0.06959858534790331"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-03T07:14:22.030Z","modified":"2026-09-03T09:43:42.516Z","0.1.0":"2026-09-03T07:14:22.412Z","0.1.1":"2026-09-03T08:03:43.396Z","0.2.0":"2026-09-03T08:09:43.103Z","0.2.1":"2026-09-03T08:22:47.470Z","0.2.2":"2026-09-03T09:43:42.191Z"},"bugs":{"url":"https://github.com/all-or-nothing-hc/impersonate/issues"},"license":"UNLICENSED","homepage":"https://github.com/all-or-nothing-hc/impersonate#readme","repository":{"type":"git","url":"git+https://github.com/all-or-nothing-hc/impersonate.git"},"description":"Generic Supabase admin \"login as user\" (impersonation) — Edge Function + React hook + banner component.","maintainers":[{"name":"impersonate","email":"carsten.markus.knopf@gmail.com"}],"readme":"# impersonate\n\nGenerisches Supabase \"Admin-Login-als-User\" (Assume Identity / Impersonation)\n— eine Edge Function + ein React-Hook + eine Banner-Komponente. Für mehrere\nProjekte gedacht (Vocab, Taskly, Trainingsapps) statt in ein einzelnes\nProjekt fest verdrahtet.\n\n> **Status: läuft produktiv in Taskly** (Start+Stop-Roundtrip bestätigt,\n> 2026-09-03). Dieses Repo selbst ist gerade im Umbau von reiner\n> Datei-Kopie zu echtem npm-Package (`@all-or-nothing-hc/impersonate`,\n> öffentlich auf npm) — Taskly nutzt aktuell noch eine lokale Kopie von\n> Client- und Server-Code, siehe \"Distribution\" unten. Ein Logik-Review-\n> Spike (2026-08-26) hat mehrere Bugs gefunden und behoben (siehe unten\n> unter \"Sicherheitsannahmen\").\n\n## Wie es funktioniert\n\n**Kein Token/keine Session wird jemals clientseitig zwischengespeichert.**\nDas einzige, was lokal (in `localStorage`) liegt, ist eine nicht-geheime\nReferenz — die `id` der Audit-Log-Zeile (`auditLogId`). Start und Stop sind\nbeide symmetrische Session-Mint-Roundtrips über den Server; die Datenbank\nist die alleinige Quelle der Wahrheit dafür, wer gerade wen impersoniert.\n\n1. Admin ruft `useImpersonation().startImpersonation(targetUserId)` auf.\n2. Der Hook ruft die Edge Function (`action: 'start'`) mit dem eigenen\n   Admin-Token auf.\n3. Die Function prüft serverseitig: ist der Aufrufer Admin? Ist das Ziel im\n   gleichen Scope (Standard: `family_id`)? Ist das Ziel selbst kein Admin?\n   Erst dann wird eine Session für das Ziel gemintet und ein Audit-Log-\n   Eintrag geschrieben (dessen `id` an den Client zurückgeht).\n4. Der Client übernimmt die zurückgegebene Session direkt\n   (`supabase.auth.setSession({ access_token, refresh_token })`) — ab hier\n   läuft die App exakt wie für den echten Ziel-User, mit allen echten\n   Konsequenzen. Lokal wird nur `{ auditLogId, email, startedAt }` in\n   `localStorage` abgelegt (Key `impersonate:active`), zusammen mit einem\n   Auto-Stop-Timer (`maxDurationMs`, Default 30 Minuten), der auch über\n   einen Reload hinweg korrekt weiterläuft (berechnet aus `startedAt`).\n5. Weil `localStorage` (nicht `sessionStorage`) verwendet wird, sieht ein\n   neuer Tab im selben Browser die aktive Impersonation sofort — ein\n   `storage`-Event-Listener hält alle offenen Tabs synchron (Banner\n   erscheint/verschwindet überall gleichzeitig, Timer wird überall\n   neu geplant).\n6. `stopImpersonation()` ruft die Function (`action: 'end'`) mit der\n   *aktuell aktiven* Session (also der des Ziel-Users) und der\n   gestashten `auditLogId` auf. Die Function verifiziert, dass der\n   Aufrufer wirklich der impersonierte Ziel-User ist, setzt `ended_at`\n   im Audit-Log (best effort) und mintet eine frische Session für den\n   ursprünglichen Admin. Der Client übernimmt auch diese Session ganz\n   normal über `setSession()` — kein Unterschied zum Start-Vorgang, nur in\n   umgekehrter Richtung. Schlägt irgendein Schritt fehl, wird niemals\n   stillschweigend die Ziel-User-Session belassen: als Fallback greift\n   `supabase.auth.signOut()`.\n\n### Session-Minting ist pluggable\n\nWie die Function eine Session tatsächlich mintet, ist server-seitig über\n`sessionMint` konfigurierbar (`ImpersonateHandlerConfig` in\n`src/server/index.ts`) — der Client sieht in beiden Fällen dieselbe\n`{ accessToken, refreshToken }`-Antwort und muss die Strategie nicht kennen:\n\n- **`{ strategy: 'generate-link' }` (Standard).** Der normale Weg für\n  Supabase Cloud und jedes gesunde self-hosted GoTrue:\n  `admin.generateLink()` erzeugt einen Magic-Link-Token, der serverseitig\n  sofort per `verifyOtp()` gegen eine echte Session eingelöst wird.\n- **`{ strategy: 'direct-db', jwtSecret, dbUrl, sessionTtlSeconds? }`\n  (optional).** Workaround für self-hosted GoTrue-Instanzen, die den\n  Upstream-Bug supabase/auth#2057 (Regression aus v2.175.0) treffen —\n  dort schlägt `admin.generateLink()` für JEDEN existierenden User mit\n  `duplicate key value violates unique constraint \"users_email_partial_key\"`\n  fehl. Diese Strategie signiert den Access-Token selbst (HS256, gleicher\n  Secret wie GoTrue — `GOTRUE_JWT_SECRET`/`PGRST_JWT_SECRET`) und schreibt\n  Session + Refresh-Token direkt per Postgres-Verbindung in\n  `auth.sessions`/`auth.refresh_tokens`. Das Paket `postgres` ist eine\n  reguläre Dependency (nicht optional) — läuft beim Auflösen von\n  `npm:@all-or-nothing-hc/impersonate/server` automatisch mit, ohne dass\n  Projekte, die diese Strategie gar nicht nutzen, dadurch nennenswert mehr\n  installieren. (War in 0.1.x/0.2.0 fälschlich eine optionale Peer-\n  Dependency — Denos npm-Resolver löst optionale Peer-Deps nicht auf,\n  solange nichts im Import-Graph sie referenziert, was `direct-db` beim\n  ersten echten Einsatz kaputt gemacht hat. Seit 0.2.1 gefixt.) Supabase-\n  Cloud-Projekte treffen diesen Bug vermutlich nicht und sollten beim\n  Standardpfad bleiben.\n\n## Setup pro Projekt\n\n**1. Edge Function deployen**\n\n```bash\nsupabase functions deploy impersonate --project-ref <dein-projekt-ref>\n```\n\nFalls eure Tabellen-/Spaltennamen von den Defaults abweichen, oder um die\nCORS-Origin einzuschränken (dringend empfohlen vor Produktiveinsatz —\nDefault ist `*`), als Secrets setzen:\n\n```bash\nsupabase secrets set \\\n  IMPERSONATE_ROLES_TABLE=user_roles \\\n  IMPERSONATE_ADMIN_COLUMN=is_admin \\\n  IMPERSONATE_SCOPE_COLUMN=family_id \\\n  IMPERSONATE_USER_ID_COLUMN=user_id \\\n  IMPERSONATE_AUDIT_TABLE=impersonation_log \\\n  IMPERSONATE_ALLOWED_ORIGIN=https://eure-app.example.com\n```\n\nNur nötig auf self-hosted GoTrue-Instanzen, die den `generateLink`-Bug\n(supabase/auth#2057) treffen — sonst weglassen, Default bleibt\n`generate-link`:\n\n```bash\nsupabase secrets set \\\n  IMPERSONATE_SESSION_STRATEGY=direct-db \\\n  JWT_SECRET=<gleicher-wert-wie-GOTRUE_JWT_SECRET> \\\n  SUPABASE_DB_URL=<direkte-postgres-connection-string> \\\n  IMPERSONATE_SESSION_TTL_SECONDS=3600\n```\n\n`postgres` müsst ihr dafür nicht separat einrichten — ist eine reguläre\nDependency des Pakets, läuft beim Import über\n`npm:@all-or-nothing-hc/impersonate/server` automatisch mit.\n\n**Nicht-boolesche Admin-Spalte** (z.B. eine `role`-Textspalte statt eines\nbooleschen `is_admin`):\n\n```bash\nsupabase secrets set \\\n  IMPERSONATE_ADMIN_COLUMN=role \\\n  IMPERSONATE_ADMIN_VALUE=admin\n```\n\n`IMPERSONATE_ADMIN_VALUE` ist der Wert, der `adminColumn` mit `===`\nvergleicht (Default: `true`, passend für eine boolesche Spalte).\n\n**Kein Scope-Konzept** (z.B. genau eine globale Admin-Ebene ohne\n`family_id`/`org_id`-Äquivalent — Vorsicht, das ist eine echte\nAutorisierungs-Entscheidung, kein Config-Detail, siehe Kommentar bei\n`scopeColumn` in `src/server/index.ts`):\n\n```bash\nsupabase secrets set IMPERSONATE_SCOPE_COLUMN=\n```\n\nEin explizit leerer Wert deaktiviert den Scope-Check komplett — dann darf\njeder Admin jeden Nicht-Admin impersonieren. Env-Var einfach weglassen\nlässt den Default (`family_id`) aktiv.\n\n**2. Migration übernehmen**\n\n`supabase/migrations/0001_impersonation_log.sql` in die eigene\nMigrations-Historie des Projekts kopieren/anpassen und ausführen.\n\n**3. Paket installieren**\n\n```bash\nnpm install @all-or-nothing-hc/impersonate\n```\n\n**4. Im Frontend verwenden**\n\n```tsx\nimport { useImpersonation, ImpersonationBar } from '@all-or-nothing-hc/impersonate'\nimport { supabase } from './lib/supabase'\n\nfunction AdminShell() {\n  // functionName defaults to 'impersonate' — only pass it if you deployed\n  // under a different name. The hook calls supabase.functions.invoke()\n  // under the hood, so it inherits the URL/apikey already configured on\n  // your `supabase` client — no separate function URL to build by hand.\n  const impersonation = useImpersonation({ supabase })\n\n  return (\n    <>\n      <ImpersonationBar\n        isImpersonating={impersonation.isImpersonating}\n        impersonatingEmail={impersonation.impersonatingEmail}\n        onStop={impersonation.stopImpersonation}\n      />\n      {/* ... restliche App ... */}\n    </>\n  )\n}\n```\n\n**5. Eigene Edge Function statt `supabase functions deploy` aus diesem Repo**\n\nWer die Function lieber im eigenen Projekt-Repo deployt (z.B. weil dort\nschon eine CI dafür existiert), kann sie als dünnen Wrapper um das\n`./server`-Subpath des Pakets bauen, statt Schritt 1 aus diesem Repo zu\nnutzen:\n\n```ts\n// supabase/functions/impersonate/index.ts im eigenen Projekt\nimport { createImpersonateHandler, impersonateConfigFromEnv } from 'npm:@all-or-nothing-hc/impersonate/server'\n\nDeno.serve(createImpersonateHandler(impersonateConfigFromEnv()))\n```\n\n`impersonateConfigFromEnv()` liest dieselben `IMPERSONATE_*`/`SUPABASE_*`/\n`JWT_SECRET`-Secrets wie oben. Alternativ `createImpersonateHandler({...})`\ndirekt mit einem Objekt aufrufen statt über Env-Vars zu konfigurieren.\n\n## Sicherheitsannahmen (nicht verhandelbar, siehe Edge Function)\n\n- Aufrufer muss `is_admin === true` sein.\n- Ziel-User muss im gleichen Scope sein (Spalte konfigurierbar).\n- Ziel-User darf selbst kein Admin sein (Privilege-Escalation-Schutz).\n- Jeder Start-Aufruf wird auditiert (`impersonation_log`), inklusive `ended_at`.\n- `maxDurationMs` (Default 30 Min) stoppt eine vergessene, offene Session\n  automatisch — aber nur clientseitig, solange der Tab offen bleibt. Das\n  ist **keine** echte Sicherheitskontrolle, nur ein Sicherheitsnetz gegen\n  \"Tab offen vergessen\".\n- Es werden keine Auth-Tokens oder Session-Objekte im Browser\n  zwischengespeichert — weder für den Ziel-User noch für den Admin. Die\n  einzige lokal gehaltene Information ist eine nicht-geheime UUID\n  (`auditLogId`), die ohne den serverseitigen DB-Lookup + Live-Auth-Check\n  wertlos ist. Wichtig dabei (Fix 2026-09-03, siehe Deep-Dive-Review):\n  `action: 'end'` prüft zusätzlich, dass die Session noch nicht beendet\n  ist (`ended_at == null`) — ohne diesen Check hätte jeder, der eine alte\n  `auditLogId` UND ein gültiges JWT des damaligen Ziel-Users kennt, beliebig\n  oft eine frische Admin-Session nachmagen können, auch lange nach dem\n  eigentlichen Ende, und ohne dass das im Audit-Log auftaucht (ein Replay\n  liest nur die bestehende Zeile, schreibt keine neue). Genau dieser Check\n  macht die \"nicht-geheim, weil wertlos ohne Server-Lookup\"-Aussage oben\n  erst wahr.\n- Ein Start ohne funktionierenden Audit-Log-Eintrag ist unmöglich\n  (fail-closed): der Audit-Insert passiert, bevor irgendein Token gemintet\n  wird; schlägt er fehl, wird der Start abgebrochen. Schlägt danach das\n  Minten fehl, wird die Audit-Zeile wieder best-effort gelöscht (kein\n  Datenmüll für nie gewährte Sessions). Das Schließen (`ended_at`) am Ende\n  bleibt bewusst best-effort — die Session ist zu diesem Zeitpunkt schon\n  auditiert, nur der Abschluss-Zeitstempel könnte fehlen, und den Admin\n  deswegen als Ziel-User stranden zu lassen wäre schlimmer.\n- Alle Autorisierungs-Ablehnungen (Aufrufer kein Admin, Ziel nicht\n  gefunden, Ziel ist selbst Admin, Ziel außerhalb des Scopes, Aufrufer ist\n  nicht der Session-Ziel-User beim Beenden) liefern nach außen dieselbe\n  generische Meldung (\"Not authorized\"). Der genaue Grund landet nur im\n  Server-Log — von außen lässt sich nicht mehr per Trial-and-Error\n  herausfinden, ob ein fremder User existiert, Admin ist oder in welchem\n  Scope er liegt.\n\n## Design-Entscheidungen aus dem Logik-Review-Spike (2026-08-26) — alle drei erledigt\n\n- **Session-Persistenz über Tab-Grenzen hinweg** — entschieden: keine\n  lokalen Sessions mehr, alles über die DB. Siehe \"Wie es funktioniert\"\n  oben.\n- **Audit-Log-Fail-Open** — entschieden: fail-closed beim Start (kein\n  Audit-Eintrag → kein Token, kein Start), best-effort/fail-open bleibt nur\n  beim `ended_at`-Update am Ende (bewusste Asymmetrie, siehe oben unter\n  \"Sicherheitsannahmen\").\n- **Fehlermeldungs-Granularität** — entschieden: eine generische\n  Autorisierungs-Meldung für alle Ablehnungsgründe, Details nur im\n  Server-Log.\n\n## Distribution\n\nÖffentliches npm-Paket (`@all-or-nothing-hc/impersonate`, `publishConfig.access: \"public\"`)\n— öffentlich, damit Deno-Edge-Functions es ohne Registry-Auth-Token per\n`npm:`-Spezifikation direkt importieren können (`npm:@all-or-nothing-hc/impersonate/server`).\nEnthält keine Secrets; alle projektspezifischen Werte (Tabellennamen,\nJWT-Secret, DB-URL) kommen über Config/Env-Vars vom Konsumenten. Zwei\nSubpaths: `.` (React-Hook + Banner-Komponente, `src/index.ts`) und\n`./server` (Edge-Function-Handler, `src/server/index.ts`).\n\nKonsumierende Projekte werden per Dependabot (`.github/dependabot.yml`,\nnpm-Ecosystem) auf neue Versionen hingewiesen — bewusst kein Silent-Auto-\nUpdate; ein PR mergen ist gleichzeitig die Benachrichtigung, dass\naktualisiert wurde. `npm publish` selbst läuft immer über Carstens eigenen\nnpm-Account, nicht automatisiert.\n","readmeFilename":"README.md"}