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

@iddp/vefk-app-kit

v0.10.0

Published

Geteiltes Backend & geteilte Logik der VEFK-Tools-Suite: Storage-Adapter, Multi-Tenant-Middleware, Auth (Passwort+2FA / Magic-Code / Passwort ohne Mandant), Mail, Upload, Export.

Readme

@iddp/vefk-app-kit

Geteiltes Backend & geteilte Logik der VEFK-Tools-Suite: Storage-Adapter, Multi-Tenant-Middleware, Auth (Passwort+2FA / Magic-Code / Passwort ohne Mandant), Mail, Upload, Export. Ein Paket, zwei Einstiegspunkte — so zieht der Frontend-Bundle nie Server-Code.

// Backend (nur dort!):
import {
  createDataStore, jsonAdapter,          // Daten-Schicht (atomar + Write-Queue)
  tenantResolver, tenantGuard, requireLogin,
  authRouter,                            // Auth-Profile hinter config.authMode: "tenant" | "generic" | "both"
  createUpload, sichererDateipfad,       // Uploads
  erstelleExcel, erstelleTenantBackup,   // Export
} from "@iddp/vefk-app-kit/server";

// Frontend UND Backend:
import { ROLLEN, darfSehen, darfEditieren, tenantPfad } from "@iddp/vefk-app-kit/shared";

Installation (npm-Paket)

npm install @iddp/vefk-app-kit
// package.json des Tools
"dependencies": {
  "@iddp/vefk-app-kit": "^0.8.0"
}

Das Paket liegt öffentlich in der npm-Registry — auf dem Server braucht npm ci dafür keinen Git- oder SSH-Zugang. Der Quellcode ist einsehbar, die Lizenz ist UNLICENSED: kein Nutzungsrecht außerhalb der VEFK-Tools-Suite. Kein Build: reines ESM, die exports-Map zeigt direkt auf die Quelldateien.

Minimal-Verdrahtung im Backend

import express from "express";
import session from "express-session";
import {
  createDataStore, jsonAdapter, authRouter, tenantResolver, tenantGuard,
} from "@iddp/vefk-app-kit/server";
import config from "../app.config.js";

const store = createDataStore({ adapter: jsonAdapter({ root: DATA_ROOT }) });
const app = express();
app.set("trust proxy", 1);
app.use(express.json({ limit: "1mb" }));
app.use(session({ /* Store/Secret ist Sache der App */ }));

app.use(sitzungPruefen({ store }));              // direkt nach session(): befristete Sitzungen (Seminar-Link)
app.use("/api/auth", authRouter({ store, config }));

const resolver = tenantResolver(store);
app.get("/api/:tenant/:datentyp", resolver, tenantGuard, async (req, res) => {
  res.json(await store.tenant(req.tenant).collection(req.params.datentyp).read({}));
});

Auth-Profile (config.authMode)

  • "tenant" — Mandanten-Tool: Passwort-Login (+ optionale 2FA), Tenant-Guard. Routen /login*. Drei Login-Wege im selben Mandanten (ab v0.6.0):
    • /login — E-Mail + Passwort (+ 2FA) für Nutzer mit passwortHash;
    • /login/magic — E-Mail-Code für Nutzer ohne Passwort (vom Admin per create-user ohne Passwort angelegt); kein Auto-Create, gleiche Antwort und gleiche Laufzeit für unbekannte E-Mails und Passwort-Nutzer (keine Enumeration), fail-closed ohne SMTP. Bestätigung über /login/code wie beim 2FA-Code;
    • /login/zugang — Mandanten-Zugang: ein gemeinsames Passwort je Mandant ohne Konto und ohne E-Mail (setzeMandantenZugang, Collection zugang, feste Rolle, kein 2FA). GET /login/optionen/:tenant → { anmeldung, konto, zugang } sagt der Login-Seite, welche Masken sie zeigt.
    • Anmeldeart je Mandant (ab v0.8.0): dashboard-config.anmeldung = "konto" (Default: Nutzerkonten, Zugang zusätzlich möglich) oder "zugang" (nur das Mandanten-Passwort; /login und /login/magic → 403). ANMELDUNGEN, anmeldungDesMandanten exportiert; der Admin-Router nimmt das Feld bei POST/PATCH /tenants.
  • "generic" — mandantenloses Tool: ein offener Bereich im internen Tenant (config.generic.tenant). Das Login-Verfahren wählt config.generic.login:
    • "magic" (Default) — passwortloser Magic-Code per E-Mail, Auto-Create (Adressen aus config.admin.emails und ownerEmail werden admin, alle anderen viewer). Routen /magic*.
    • "password" (ab v0.4.0) — „normaler" Login mit E-Mail + Passwort, optional 2FA (config.generic.zweiFaktor), kein Auto-Create: Nutzer legt der Admin an (create-user). Routen /password*; Nutzer ohne Passwort melden sich per E-Mail-Code an (/password/magic → /password/code, ab v0.6.0).
    • Seminar-Link (ab v0.9.0, zusätzlich zu magic/password) — befristeter Zugang ohne Konto: der Admin legt ein Seminar mit Zeitfenster an (legeSeminarAn, Collection seminare im generic-Tenant), der Link …/login?seminar=<code> wird per POST /seminar { code } eingelöst. Anonyme Sitzung (email: null, feste Rolle, nie admin) mit seminar: { id, name, bis } und gueltigBis. 401 für unbekannte Codes, 403 mit Grund für geplant/beendet/deaktiviert. Ohne angelegte Seminare ist der Weg einfach leer — kein Config-Schalter. Die App kann über user.seminar.id selbst ein Kontingent zählen. Ein eigener Code (legeSeminarAn(…, { code }), ab v0.9.1) hat 6–16 Zeichen (ab v0.9.2); findeSeminarMitStatus (ab v0.9.2) findet ein Seminar per Code samt Status.
  • "both" (ab v0.2.0) — beides parallel: Mandanten-Bereiche wie bei tenant plus der offene Bereich (Magic-Code oder Passwort, s. o.). Die Auth-Routen sind disjunkt (/login* vs. /magic* bzw. /password*), die Nutzerkreise auch — Nutzer des offenen Bereichs leben nur im generic-Tenant und kommen am Tenant-Guard nicht vorbei. Der Seminar-Link gilt nur für den offenen Bereich. Wichtig bei der Verdrahtung: die offenen /api/data/…-Routen vor den /api/:tenant/…-Routen registrieren (sonst würde data als Slug gelesen).

Vollständige Muster (Session-Store, Static-Serving, Error-Handler, Uploads, Export-Routen) zeigt das vefk-app-template — neue Tools entstehen daraus, nicht von Hand.

Datenspeicher (createDataStore)

Zwei Adapter, ein Vertrag — der Tool-Code merkt den Unterschied nicht:

import { createDataStore, jsonAdapter, sqliteAdapter, migriereJsonNachSqlite } from "@iddp/vefk-app-kit/server";
const store = createDataStore({ adapter: sqliteAdapter({ root: dataRoot }) }); // <root>/app.sqlite
// oder: jsonAdapter({ root: dataRoot })                                         // <root>/tenants/<slug>/*.json
await store.tenant("kunde-a").collection("daten").read({});
await store.tenant("kunde-a").collection("daten").write(daten);
await store.tenant("kunde-a").collection("zugang").delete();
await store.createTenant("kunde-b");   // aus _muster (ohne users/zugang/seminare), legt Dokumente/ an
await store.deleteTenant("kunde-b");   // Collections + Dateiablage
await migriereJsonNachSqlite({ root, file }); // einmalig, idempotent, JSON bleibt liegen

sqliteAdapter (better-sqlite3, ab 0.7.0): eine Datei, WAL, Transaktionen; Dokumente bleiben Dateien unter tenants/<slug>/Dokumente/. Das native Modul wird erst beim Aufruf geladen. Die Vorlage _muster ist Code, nicht Daten: createTenant liest sie immer von Platte (tenants/_muster/*.json), Migration und Import lassen sie aus — sie steht nie in der DB.

Admin-Bereich (adminRouter, ab 0.7.0)

app.use("/api/admin", adminRouter({ store, config }));
// config.admin = { emails: ["[email protected]"], pfad: "/admin" }
// GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET (optional — sonst E-Mail-Code über SMTP) stehen
//   suite-weit in /etc/vefk-suite/env (Server) bzw. ~/.config/vefk-suite/env (Rechner) —
//   server.js lädt sie nach der eigenen .env über ladeToolUndSuiteEnv(); Redirect-URI kommt
//   aus dem Request-Host (mehrere Domains), GOOGLE_REDIRECT_URI erzwingt einen festen Wert

Eigene Session (req.session.admin), Login per Google-Konto (OAuth 2.0 / OIDC, natives fetch) oder E-Mail-Code; nur für die Admin-Liste (config.admin.emails + Ergänzungen über die API). Routen: Mandanten anlegen/ändern/löschen (Tipp-Bestätigung), Nutzer je Mandant (anlegen, ändern, Passwort, fester Code, deaktivieren, Auskunft, anonymisieren), Mandanten-Zugang, Seminar-Links (nur generic/both: GET/POST /seminare, PATCH/DELETE /seminare/:id, POST /seminare/:id/neuer-link — Antworten enthalten den Code, das Audit nie), Admins, Audit. requireAdmin für tool-eigene Admin-Routen, RESERVIERTE_SLUGS für den Scaffolder. Kein Bypass auf Mandanten-Daten. → Template docs/09.

Fester Code (setzeFestenCode(store, tenant, id, "123456")): ein Nutzer ohne Passwort meldet sich über den Code-Weg an, bekommt aber keine Mail — der hinterlegte bcrypt-Hash wird zum Pending-State. Braucht kein SMTP. Einordnung: kurzes Passwort ohne Ablauf, nur für Demo-/ Kiosk-Nutzer. → Template docs/08.

Gemeinsam für alle Profile: GET /me, PATCH /me { name } (eigener Anzeigename; nicht für den Mandanten-Zugang und den Seminar-Link), POST /logout.

Befristete Sitzungen (ab v0.9.0): trägt der Session-Nutzer gueltigBis, lehnen requireLogin, tenantGuard und GET /me ihn nach Ablauf ab (401) und räumen die Sitzung ab — synchron, auch ohne weitere Middleware. sitzungPruefen({ store }) (App-weit direkt nach session()) gleicht Seminar-Sitzungen zusätzlich bei jedem Request gegen die Datei ab: Deaktivieren, Löschen und „Neuer Link" melden sofort ab, Verlängern und Rollenwechsel ziehen laufende Sitzungen nach. Das Cookie behält die App-Laufzeit; maßgeblich ist der Server.

Sicherheits-Invarianten (getestet in tests/)

  • Tenant-Slug-Validierung (^[a-z0-9-]+$ + Existenz-Check) bevor ein Pfad entsteht
  • Atomares Schreiben (Temp + rename) + Write-Queue pro Datei
  • Tenant-Guard serverseitig, kein Admin-Bypass
  • Login-Rate-Limit pro IP und pro E-Mail; bei Passwort-Logins zählen nur Fehlversuche (nurFehlversuche), beim Code-Anfordern jeder Versuch
  • bcrypt-Cost 12 für Passwörter, Cost 8 nur für kurzlebige Codes
  • 2FA/Magic-Code fail-closed (kein SMTP + nicht Dev → 503)
  • Code-Login pro Nutzer nur ohne passwortHash; Passwort-Nutzer und unbekannte E-Mails bekommen dieselbe Antwort ohne Code (keine Enumeration, kein Auto-Create)
  • Admin-Bereich: eigene Session, kein Tenant-Guard-Bypass, _admin nie einloggbar/listbar, Google-state gegen CSRF, id_token auf Aussteller/Audience/Ablauf/email_verified geprüft, Antworten ohne Hashes, Audit ohne Geheimnisse; fester Code nur für Nutzer ohne Passwort
  • Mandanten-Zugang: Hash in zugang.json (nie im Git), 401 für „kein Zugang" und „falsches Passwort", Rate-Limit pro IP + Mandant (nur Fehlversuche, 20/15 min), Session ohne E-Mail; setzeMandantenZugang nur für vorhandene Mandanten (code: "TENANT"), Entfernen löscht die Datei (collection.delete())
  • Seminar-Link: ≈59-Bit-Code (crypto, Rejection Sampling), Plausibilitätsprüfung vor jedem Store-Zugriff, Vergleich in konstanter Zeit; Rate-Limit pro IP, das nur unbekannte Codes zählt (ein Kurs hinter einer IP sperrt sich nicht selbst aus); Rolle nie admin; seminare.json nie im Git und nie aus _muster kopiert; Code-Fingerabdruck nur serverseitig in der Session (nicht in /me)
  • Befristete Sitzungen: abgelaufenes gueltigBis → 401 in allen Guards, auch ohne sitzungPruefen; Deaktivieren/Löschen/Neuer Link wirkt mit sitzungPruefen sofort
  • Dateinamen-Sanitisierung + Pfad-Containment für Upload/Download
  • Keine personenbezogenen Daten in Logs: ohne SMTP wirft send() außerhalb von Dev (MAIL_NOT_CONFIGURED) statt Adresse + Code in die Konsole zu schreiben
  • Mail-Header minimal: send({ to, subject, text, html }) setzt keine defensiven Zusatz-Header. Einzige Option (ab v0.5.0): replyTo für Mails, auf die Menschen antworten sollen (Einladungen, Benachrichtigungen). Code-Mails (sendLoginCode) bleiben ohne.
  • Betroffenenrechte eingebaut: deaktiviereUser, loescheUser (anonymisiert, id bleibt; { hart: true } entfernt) und exportiereUserdaten (Auskunft ohne Passwort-Hash) — das Template ruft sie über npm run user auf (→ Template docs/05)

Entwicklung

npm install && npm test

Regeln für Änderungen an diesem Repo: AGENTS.md.