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

@mostajs/booking-checkin-qr

v0.1.0

Published

QR code check-in pour @mostajs/booking. Génère un QR contenant le joinToken HMAC d'une reservation, scan côté staff valide + marque attended. Backend via @mostajs/qrpanel (server-side PNG/SVG, no Chromium).

Readme

@mostajs/booking-checkin-qr

Auteur : Dr Hamid MADANI [email protected] License : AGPL-3.0-or-later Version : 0.1.0

Check-in QR code pour @mostajs/booking. Génère un QR contenant l'URL signée HMAC d'une reservation ; staff scanne à l'arrivée, l'app valide + transitionne automatiquement vers attended. Backend QR via @mostajs/qrpanel (server-side PNG/SVG, no Chromium, 12 thèmes prêts à l'emploi).

Table des matières

  1. Pourquoi le QR pour check-in
  2. Flow complet
  3. Quick start
  4. API
  5. Sécurité
  6. Patterns
  7. Cas d'usage
  8. Limites v0.1
  9. Modules liés

1. Pourquoi le QR pour check-in

Aux restaurants, événements, cabinets médicaux, événements sportifs, salles de cours, le check-in se fait souvent au moment de l'arrivée :

  • Hôtesse / staff scanne le QR du client → enregistre l'arrivée
  • Borne tablet en self-service où le client scanne → check-in solo
  • Portique d'entrée connecté → contrôle automatique

Sans QR : staff doit demander email/nom + lookup base → lent + sujet aux erreurs. Avec QR : scan instantané, identification certaine, audit trail automatique.

Modèle de menace :

  • ✅ Token HMAC signé (reservation.joinToken déjà fourni par @mostajs/booking) → impossible à forger
  • ✅ Constant-time compare (déjà fait côté manager.verifyJoinToken)
  • ✅ Transition unique confirmed → attended (impossible 2× = idempotent + audit)
  • ⚠️ Le QR contient le token en clair dans le QR → ne pas afficher publiquement (mail privé OK)
  • ⚠️ Staff scanner doit être AUTH (rôle 'staff'/'host') AVANT d'appeler scanAndAttend

2. Flow complet

┌─────────────────────────────────────────────────────────────┐
│  1. Reservation confirmed (via booking core)                 │
│     reservation.joinToken = HMAC signé (déjà disponible)     │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  2. App appelle adapter.generateQR(reservationId, 'png')     │
│     → Buffer PNG contenant URL                               │
│     https://app.example.com/checkin?reservationId=X&token=Y  │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
                  Mail / Dashboard / Apple Wallet
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  3. Client présente QR à l'arrivée                           │
│     Staff scanne avec téléphone (camera native)              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
       GET https://app.example.com/checkin?reservationId=X&token=Y
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  4. Route Next.js → adapter.checkinHandler(req)              │
│     a. authorizeScanner(req, reservation) : staff role OK ?  │
│     b. manager.verifyJoinToken(id, token) : HMAC valid ?     │
│     c. reservation.status === 'confirmed' ?                  │
│     d. manager.attendReservation(id) → status='attended'     │
│     e. Response { ok: true, reservation }                    │
└─────────────────────────────────────────────────────────────┘

3. Quick start

Installation

npm install @mostajs/booking @mostajs/qrpanel @mostajs/booking-checkin-qr

Setup

import { createBookingCheckinQR } from '@mostajs/booking-checkin-qr'
import * as qrpanel from '@mostajs/qrpanel'

const checkin = createBookingCheckinQR({
  manager,                                  // BookingManager
  qr: qrpanel,                              // pass module entier (PNG/SVG/DataURL helpers)
  baseUrl: 'https://app.example.com',
  checkinPath: '/checkin',                  // default
  qrDefaults: { width: 600, errorCorrectionLevel: 'H' },
  authorizeScanner: async (req, reservation) => {
    const session = await getServerSession(req)
    if (!session) return new Response('Unauthorized', { status: 401 })
    if (!session.user.roles.includes('staff')) {
      return new Response('Forbidden — staff role required', { status: 403 })
    }
    // Optional : vérifier que ce staff est lié à cette resource
    return null  // autorisé
  },
  onScanned: (reservation) => console.log('[checkin]', reservation.id, 'attended'),
  onCheckinFailed: (reason, id) => console.warn('[checkin] failed', reason, id),
})

Générer QR au moment de la confirmation

// app/api/booking/reservations/[id]/qr/route.ts
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const png = await checkin.generateQR(id, 'png')
  if (!png) return new Response('Reservation not found', { status: 404 })
  return new Response(png, {
    status: 200,
    headers: { 'content-type': 'image/png', 'cache-control': 'private, max-age=300' },
  })
}

→ user voit dans son dashboard <img src="/api/booking/reservations/abc/qr"> qui affiche le QR.

Route de check-in

// app/api/checkin/route.ts
export async function GET(req: Request) {
  return checkin.checkinHandler(req)
}

Quand le staff scanne le QR avec son téléphone, le navigateur ouvre directement cette URL → reservation transitionne en attended.

Inclure le QR dans le mail de confirmation

// Combiné avec @mostajs/booking-notifications
import { BOOKING_NOTIFICATION_KINDS } from '@mostajs/booking-notifications'

adapter.setTemplate(BOOKING_NOTIFICATION_KINDS.RESERVATION_CONFIRMED, {
  async render(ctx) {
    const reservation = ctx.reservation as any
    const qrDataUrl = await checkin.generateQR(reservation.id, 'dataUrl')
    return {
      to: (ctx as any).email,
      subject: 'Votre réservation confirmée',
      html: `
        <h1>Réservation confirmée</h1>
        <p>Présentez ce QR à l'arrivée :</p>
        <img src="${qrDataUrl}" alt="QR check-in" width="300">
      `,
    }
  },
})

4. API

createBookingCheckinQR(opts) → BookingCheckinQRAdapter

interface BookingCheckinQROptions {
  manager: BookingManager
  qr: QrGenerator                          // @mostajs/qrpanel ou compatible

  baseUrl: string                          // 'https://app.example.com'
  checkinPath?: string                     // '/checkin' default

  qrDefaults?: QrOptions
  authorizeScanner?: (req, reservation) => Promise<Response | null> | Response | null
  onScanned?:        (reservation, req) => void | Promise<void>
  onCheckinFailed?:  (reason, reservationId?) => void | Promise<void>
}

interface BookingCheckinQRAdapter {
  generateQR(reservationId: string, format?: 'png' | 'svg' | 'dataUrl', opts?: QrOptions):
    Promise<Buffer | string | null>
  buildCheckinUrl(reservation: Reservation): string
  scanAndAttend(input: { reservationId: string; token: string }): Promise<ScanResult>
  checkinHandler(req: Request): Promise<Response>
}

type ScanResult =
  | { ok: true;  reservation: Reservation }
  | { ok: false; reason: 'invalid-token' | 'not-found' | 'wrong-status' | 'already-attended'; status?: number }

QrGenerator interface

interface QrGenerator {
  generateQrPng(text: string, opts?: QrOptions): Promise<Buffer>
  generateQrSvg(text: string, opts?: QrOptions): Promise<string>
  generateQrDataUrl(text: string, opts?: QrOptions): Promise<string>
}

@mostajs/qrpanel v0.4+ implémente cette interface directement.

URL construite

${baseUrl}${checkinPath}?reservationId=<id>&token=<joinToken>

Exemple : https://app.example.com/checkin?reservationId=abc-123&token=<base64url-sig>


5. Sécurité

Layers de défense

| Layer | Mécanisme | Module | |---|---|---| | Token forgé | HMAC SHA-256 + constant-time compare | @mostajs/booking (core) | | Replay attack (réutiliser un QR) | Status check confirmed → attended (one-way) | @mostajs/booking (state machine) | | Token sniff (man-in-middle) | HTTPS obligatoire | App + reverse proxy | | Staff non autorisé scan | authorizeScanner callback (RBAC) | App + @mostajs/rbac | | Brute-force token | Pas de bruteforce praticable (HMAC 256 bits) | crypto strength | | QR partagé entre users | Le token est lié à 1 reservation = 1 user ; multi-scan = même result | state machine |

Bonnes pratiques

  • HTTPS obligatoire pour la route check-in (Let's Encrypt / Caddy / Nginx)
  • authorizeScanner strict : seuls les users avec rôle staff ou host (de la resource concernée)
  • Audit log : onScanned callback vers @mostajs/audit pour traçabilité RGPD
  • Rate limit la route check-in côté reverse proxy / middleware
  • Ne JAMAIS afficher le QR publiquement (URL avec token = bypass auth)
  • Ne pas mettre le token dans les server logs (filtrer URL avant console.log)

6. Patterns

Pattern A — Generation lazy (PNG cached à la 1ère request)

const qrCache = new Map<string, Buffer>()

export async function GET(_req, { params }) {
  const { id } = await params
  const cached = qrCache.get(id)
  if (cached) return new Response(cached, { headers: { 'content-type': 'image/png' }})
  const png = await checkin.generateQR(id, 'png')
  if (!png) return new Response('Not found', { status: 404 })
  qrCache.set(id, png as Buffer)
  return new Response(png, { headers: { 'content-type': 'image/png' }})
}

→ Cache mémoire OK car le QR ne change pas (même token = même URL = même bytes).

Pattern B — QR signé à la volée dans le mail (DataURL embedded)

// Pas de roundtrip HTTP : le QR est inline dans le mail HTML
const dataUrl = await checkin.generateQR(reservation.id, 'dataUrl')
const html = `<img src="${dataUrl}" alt="QR check-in">`

⚠️ Limite : certains clients mail bloquent les data URLs (Gmail web ok, Outlook 2019 KO). Sinon → uploader sur CDN puis href absolue.

Pattern C — Thème custom (logo entreprise au centre du QR)

// @mostajs/qrpanel supporte 12 thèmes (logo overlay, frame, etc.)
const png = await checkin.generateQR(id, 'png', { theme: 'studio-dark' })

→ ECC=H (30% recovery) déjà default, donc tolérant à l'overlay logo.

Pattern D — Borne self-service (tablet caisse)

<!-- public/checkin-station.html -->
<input id="manual-token" placeholder="Ou tapez l'ID manuellement">
<button onclick="manual()">Check-in</button>
<script>
async function manual() {
  const [id, token] = document.getElementById('manual-token').value.split('|')
  const r = await fetch('/api/checkin', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ reservationId: id, token }),
  })
  // ...
}
</script>

checkinHandler accepte body JSON aussi ({ reservationId, token }).

Pattern E — Walk-in / no-show flow

// Après l'horaire de la session, marquer no-show les confirmed non-attended
setInterval(async () => {
  const past = await manager.listReservations({ status: ['confirmed'] })
  for (const r of past) {
    const slot = await manager.listPersistedSlots({}).then(s => s.find(x => x.id === r.slotId))
    if (slot && slot.endAt < Date.now() && !r.attendedAt) {
      await manager.markNoShow(r.id)
    }
  }
}, 5 * 60_000)  // every 5 min

→ Pas spécifique au QR mais complémentaire : le scan check-in et le mark no-show forment la state machine complete.


7. Cas d'usage

🍽️ Restaurant (OpenTable-like)

  • Client réserve une table 4 couverts via Cal-style flow
  • Mail de confirmation contient QR (data URL embedded)
  • À l'arrivée, hôtesse scanne avec sa tablette → attended + service débute
  • No-show 30 min après slot.startAt → markNoShow automatique (free table for walk-ins)

🎫 Événement (Eventbrite-like)

  • Achat ticket → reservation pending → paiement OK → confirmed
  • Ticket QR émis (Apple Wallet pass + PNG dans le mail)
  • Entrée : portique scanne → permet l'accès
  • Multi-scan d'un même QR : 1er succès, suivants retournent { ok: true, alreadyAttended: true } (idempotent)

🩺 Cabinet médical (Doctolib-like)

  • Patient reçoit le QR par mail
  • Borne d'accueil tablet : patient scanne lui-même son QR → check-in + ticket d'appel imprimé
  • Praticien voit en temps réel les patients arrivés dans la salle d'attente

🎓 Cours / formation

  • Étudiant inscrit à une session live SFU + presentiel
  • Mail de rappel J-1 inclut QR
  • À l'entrée de la salle physique : enseignant scanne pour valider la présence
  • Attendance = base pour notation continue / attestation présence

🏋️ Salle de sport / cours collectifs

  • Adhérent réserve un créneau yoga (1 sur 20 places)
  • Portique d'entrée scan QR → valide créneau + déduit 1 séance du pack

8. Limites v0.1

  • Pas de wallet Pass natif (Apple Wallet .pkpass, Google Pay) — l'app génère manuellement si besoin
  • Pas de notification push à l'arrivée (host = "X vient d'arriver") — utiliser onScanned + @mostajs/notifications-push futur
  • Pas de retour visuel d'erreur custom dans le QR scan (handler retourne JSON, pas HTML rendered) — l'app peut wrap checkinHandler pour rendre une page HTML
  • Manager listReservations linéaire : pour large volumes, l'app peut wrapper avec un cache mémoire (cf. Pattern A pour les QRs)
  • Pas de waitlist auto-promote au no-show — combinaison avec @mostajs/booking-waitlist futur

9. Modules liés


License : AGPL-3.0-or-later Auteur : Dr Hamid MADANI [email protected]