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

@mrs-maid/verify

v1.0.0

Published

Client fuer das Mrs. Maid Verify Gateway: Sessions minten, Ergebnisse pollen, signierte Webhooks pruefen.

Downloads

15

Readme

@mrs-maid/verify

Client fuer das Mrs. Maid Verify Gateway. Session minten, Link an den User schicken, Ergebnis per Webhook oder Polling einsammeln.

Node ab 18, ESM und CJS, keine Runtime-Dependencies. node:crypto wird nur im Webhook-Entry geladen, ein reiner Bot-Prozess zieht den Crypto-Teil nicht mit.

Installation

npm install @mrs-maid/verify

Session minten

import { MrsMaidVerify } from '@mrs-maid/verify';

const verify = new MrsMaidVerify({ apiKey: process.env.MRSMAID_VERIFY_KEY! });

const session = await verify.createSession({
  guildId: interaction.guildId,
  userId: interaction.user.id,
  captcha: true,
  meta: { caseId: 'abc-123' },
});

await interaction.user.send(`Verifizierung: ${session.verifyUrl}`);

session.captcha sagt, was tatsaechlich aktiv ist. Bei einem Plan ohne Captcha steht da false, auch wenn der Request true geschickt hat. Zeig immer den Serverwert an, nicht den eigenen Wunsch.

session.expiresAt sind Unix-Sekunden, per Default 15 Minuten. Danach verfaellt die Session still. Das Gateway schreibt sie nicht auf einen eigenen Status um, sie bleibt pending.

Ergebnis einsammeln

Zwei Wege. Nimm beide.

Webhook

Der Webhook ist der schnelle Weg, aber nicht garantiert: die Retry-Schleife lebt im Prozess des Gateways, ein Neustart verwirft laufende Wiederholungen.

import express from 'express';
import { expressWebhook } from '@mrs-maid/verify/webhook';

app.post(
  '/mrsmaid/verify',
  express.raw({ type: 'application/json', limit: '64kb' }),
  expressWebhook({
    secret: process.env.MRSMAID_VERIFY_SECRET!,
    onEvent: async (result) => {
      if (result.verdict === 'kick') await guild.members.kick(result.userId);
      else if (result.isVerified) await member.roles.add(verifiedRole);
    },
  }),
);

Der Handler antwortet 204 und arbeitet onEvent erst danach ab. Der Sender bricht nach 8 Sekunden ab und stellt sonst unnoetig erneut zu.

Zwei Dinge, an denen Integrationen regelmaessig scheitern:

  • express.json() zerstoert die Signaturpruefung. Signiert wird der rohe Body. Auf dieser Route muss express.raw stehen, sonst kommt bei jeder Zustellung 400 zurueck.
  • Der Empfaenger muss idempotent sein. Bei einem Timeout kommt derselbe sessionId mehrfach an. Nimm ihn als Idempotenz-Key.

Ohne Express geht es genauso, nur von Hand:

import { verifyWebhook, WebhookVerificationError } from '@mrs-maid/verify/webhook';

try {
  const result = verifyWebhook({
    secret: process.env.MRSMAID_VERIFY_SECRET!,
    rawBody,                                  // string oder Buffer, ungeparst
    signature: headers['x-mrsmaid-signature'],
    timestamp: headers['x-mrsmaid-timestamp'],
  });
} catch (error) {
  if (error instanceof WebhookVerificationError) {
    // 'bad_signature' | 'stale_timestamp' | 'malformed'
    console.warn(error.reason);
  }
}

Geprueft wird in dieser Reihenfolge: Timestamp gegen die eigene Uhr, Toleranz 300 Sekunden, dann die Signatur mit timingSafeEqual, dann erst JSON.parse. Wer zuerst parst, laesst fremdes JSON in den eigenen Code.

Die Webhook-URL muss oeffentliches HTTPS sein. localhost, .local, .internal, private IPv4-Bereiche und IPv6-Literale lehnt das Gateway ab. Lokal entwickeln heisst also Tunnel oder Polling.

Polling

waitForResult pollt bis completed oder bis das Zeitbudget leer ist. Ohne timeoutMs nimmt der Client den Rest bis expiresAt der geminteten Session.

import { VerifyApiError } from '@mrs-maid/verify';

try {
  const result = await verify.waitForResult(session.sessionId);
} catch (error) {
  if (error instanceof VerifyApiError && error.code === 'timeout') {
    // User hat den Link nicht angefasst
  }
}

Polling verbraucht keine Quota. Weil der Webhook einen Gateway-Neustart nicht ueberlebt, gehoert ein Reconcile-Lauf ueber getSession in jeden Bot, der Ergebnisse nicht verlieren darf: offene Sessions aus der eigenen Datenbank holen, nachfragen, fertig.

const state = await verify.getSession(sessionId);
if (state.status === 'completed') await apply(state);

Was im Ergebnis steht

Der Plan des Tenants entscheidet, welche Felder geliefert werden. Alle Tier-Felder sind deshalb optional typisiert.

| Tier | Plan | Felder | |---|---|---| | 0 | alle | isVerified, isInDiscord | | 1 | ab indie | isAlt, altUserIds, trustScore, signals, deviceSimilarity, deviceClusterSize, correlation, verdict, thresholds | | 2 | ab pro | penalties, telemetry, captchaPassed, altMatches |

event, sessionId, guildId, userId, meta und completedAt kommen immer, das sind deine eigenen Daten.

Ein fehlendes Feld ist kein Fehler, sondern der Plan. trustScore ?? 100 ist ein Bug: undefined heisst "nicht gekauft", nicht "sauber".

Messung gegen Policy

deviceSimilarity, deviceClusterSize, altMatches, correlation und trustScore sind Messwerte und haengen an keiner Kundeneinstellung. verdict ist daraus abgeleitet, mit den Schwellen aus thresholds. Beide kommen mit, damit du verdict nachrechnen oder durch eine eigene Regel ersetzen kannst.

const verdict =
  result.deviceSimilarity !== undefined && result.deviceSimilarity >= 85 ? 'kick' : 'verify';

Drei Fallen

  • isVerified: true heisst nur "kein angefordertes Captcha durchgefallen". Es ist kein Freigabesignal. Die Entscheidung faellt ueber verdict oder eigene Schwellen.
  • altUserIds enthaelt fremde Discord-IDs. Wer die in einen oeffentlichen Channel schreibt, veroeffentlicht die Verknuepfung zweier Accounts. Mod-Log statt Public-Embed.
  • correlation.burstNetworkCount ist bewusst keine Alt-Aussage. Hinter Uni-NAT oder Mobilfunk-CGNAT sind das systematisch Fremde.

Limits

POST /session verbraucht eine Einheit der Tagesquota, Polling nicht. Nach jedem Mint steht der Stand am Client:

await verify.createSession({ guildId, userId });
verify.rateLimit; // { limit: 500, remaining: 487, resetInSeconds: 48213 }

Fehler

Alles, was vom Gateway kommt, wirft VerifyApiError mit code, status und retryable. Bei quota_exceeded haengen zusaetzlich limit, resetInSeconds und retryAfterSeconds dran.

try {
  await verify.createSession({ guildId, userId });
} catch (error) {
  if (error instanceof VerifyApiError && error.code === 'quota_exceeded') {
    log.warn(`Quota leer, zurueck in ${error.resetInSeconds} s`);
  }
}

Codes: bad_request, meta_too_large, bad_json, missing_api_key, invalid_api_key, tenant_suspended, not_found, payload_too_large, rate_limited, quota_exceeded, internal_error, unavailable, network_error, bad_response, timeout.

guildId, userId und die Groesse von meta prueft der Client lokal. Diese Fehler kosten keinen Netzaufruf und keine Quota-Einheit.

Wiederholt wird automatisch bei rate_limited, internal_error und unavailable, per Default zweimal, mit Retry-After wenn der Server einen schickt. Nach Timeout oder Verbindungsabbruch wiederholt der Client nur GET. Ein POST /session koennte den Server trotzdem erreicht haben, und ein blinder zweiter Versuch bucht eine zweite Session.

API

new MrsMaidVerify({
  apiKey: string,       // mm_vrf_..., nicht das Webhook-Secret
  baseUrl?: string,     // Default 'https://mrs-maid.cc'
  fetch?: typeof fetch,
  timeoutMs?: number,   // Default 10000, pro HTTP-Aufruf
  maxRetries?: number,  // Default 2
});

verify.info(): Promise<ApiInfo>
verify.createSession(input): Promise<CreatedSession>
verify.getSession(sessionId): Promise<SessionState>
verify.waitForResult(sessionId, { intervalMs?, timeoutMs?, signal? }): Promise<VerifyResult>
verify.rateLimit: RateLimitInfo | null

Aus @mrs-maid/verify/webhook:

verifyWebhook({ secret, rawBody, signature, timestamp, toleranceSeconds? }): VerifyResult
expressWebhook({ secret, onEvent, onError?, toleranceSeconds? }): RequestHandler
expectedSignature(secret, rawBody, timestamp): string
deriveWebhookKey(secret): string
WebhookVerificationError

Der API-Key und das Signing-Secret sind zwei verschiedene Dinge. Der Key mintet Sessions, das Secret prueft Webhooks. Zwei getrennte Env-Variablen.

Entwicklung

npm install
npm test          # vitest
npm run typecheck # tsc --noEmit
npm run build     # tsup, ESM + CJS + Typen
npm run check     # alle drei, laeuft auch vor npm publish

Einzelner Test:

npx vitest run test/webhook.test.ts
npx vitest run -t "rejects a single changed byte"

Lizenz

MIT