Event-Driven Integration Guide

Overview

The MasterKey2 SDK provides a complete event-driven architecture that allows you to build completely decoupled authentication UIs. Instead of using callbacks, you listen to events and update your UI accordingly.

This guide covers the why and how of the event-driven approach. For the full API reference (all methods, types, components, config), see SDK_DOCUMENTATION.md.

Why Event-Driven?

Traditional Callback Approach

await auth.passkey.authenticate({
  onSuccess: (result) => { /* update UI */ },
  onError: (error) => { /* update UI */ },
});

Limitations:

  • Callbacks are scoped to one call site
  • Hard to coordinate multiple UI components
  • Difficult to add cross-cutting concerns (analytics, logging)
  • Tight coupling between trigger and handler

Event-Driven Approach

// Set up listeners once
auth.on(MK2Events.AUTH_SUCCESS, (e) => { /* update UI */ });
auth.on(MK2Events.AUTH_ERROR, (e) => { /* update UI */ });

// Trigger actions without callbacks
await auth.passkey.authenticate(); // Events handle the rest

Benefits:

  • Complete decoupling — listeners don’t know about triggers
  • Multiple listeners per event
  • Easy to add/remove listeners at runtime
  • Natural fit for component-based frameworks (React, Vue, Svelte)
  • Cross-cutting concerns (analytics, logging) are trivially added

When to Use Which Approach

ApproachBest for
CallbacksSimple apps, single handler per action, quick prototyping
EventsMulti-component UIs, framework integration, analytics, complex flows
Web ComponentsZero-JS HTML, drop-in UI, server-rendered pages

All three work together. You can mix callbacks and events in the same app — both fire for every action.

Event System API

import MasterKey2, { MK2Events } from './masterkey2.js';

const auth = new MasterKey2({
  apiBaseUrl: 'https://auth.example.com'
});

// Add event listener
auth.on(MK2Events.AUTH_SUCCESS, handler);

// Remove event listener
auth.off(MK2Events.AUTH_SUCCESS, handler);

// One-time listener (auto-removes after first call)
auth.once(MK2Events.AUTH_SUCCESS, handler);

All handlers receive a CustomEvent with a typed detail property:

auth.on(MK2Events.QR_GENERATED, (event) => {
  console.log(event.detail.qrUrl);
  console.log(event.detail.challengeId);
});

Events Reference

Global Events

These fire for any authentication method (passkey, QR, password). Use them for common actions like redirecting or showing errors.

MK2Events.AUTH_SUCCESS

Fired when any authentication succeeds.

auth.on(MK2Events.AUTH_SUCCESS, (e) => {
  console.log('User:', e.detail.user);
  window.location.href = e.detail.redirectUrl || '/';
});

Detail:

{
  redirectUrl?: string;
  user?: {
    id: string;           // bv-masterkey2 internal ID
    external_id: string;  // Tenant-provided identifier (email, username, etc.)
    display_name?: string;
  };
}

MK2Events.AUTH_ERROR

Fired when any authentication fails.

auth.on(MK2Events.AUTH_ERROR, (e) => {
  if (e.detail.code === 'user_cancelled') return; // User dismissed dialog
  showErrorMessage(e.detail.error.message);
});

Detail:

{
  error: Error | string;
  code?: string;  // MK2ErrorCode value (e.g. 'user_cancelled', 'server_unreachable')
}

Passkey Events

MK2Events.PASSKEY_START

Fired when passkey authentication begins (before the browser dialog appears).

auth.on(MK2Events.PASSKEY_START, (e) => {
  showSpinner();
  disableButton();
});

Detail: { timestamp: number }

MK2Events.PASSKEY_SUCCESS

Fired when passkey authentication succeeds. AUTH_SUCCESS also fires.

auth.on(MK2Events.PASSKEY_SUCCESS, (e) => {
  console.log('Passkey auth for:', e.detail.user?.external_id);
});

Detail:

{
  redirectUrl?: string;
  user?: { id: string; external_id: string; display_name?: string };
}

MK2Events.PASSKEY_ERROR

Fired when passkey authentication fails. AUTH_ERROR also fires.

auth.on(MK2Events.PASSKEY_ERROR, (e) => {
  if (e.detail.code === 'user_cancelled') return;
  console.error('Passkey error:', e.detail.error.message);
});

Detail: { error: Error | string, code?: string }

QR Code Events

MK2Events.QR_GENERATED

Fired when a QR code is created and ready to display.

auth.on(MK2Events.QR_GENERATED, (e) => {
  document.getElementById('qr-img').src = e.detail.qrUrl;
});

Detail: { qrUrl: string, challengeId: string }

MK2Events.QR_STATUS_CHANGE

Fired when the cross-device status changes (mobile scanned, completed, etc.).

auth.on(MK2Events.QR_STATUS_CHANGE, (e) => {
  if (e.detail.status === 'scanned') {
    showMessage('QR scanned! Complete authentication on your phone...');
  }
});

Detail: { status: string, message?: string }

Status values: pending | scanned | completed | expired | cancelled | failed | error

MK2Events.QR_SUCCESS

Fired when QR authentication completes. AUTH_SUCCESS also fires.

Detail:

{
  redirectUrl?: string;
  user?: { id: string; external_id: string; display_name?: string };
}

MK2Events.QR_ERROR

Fired when QR authentication fails. AUTH_ERROR also fires for non-cancellation errors.

When code is 'user_cancelled', the mobile user dismissed the WebAuthn prompt. The composite components auto-regenerate a fresh QR in this case.

Detail: { error: Error | string, code?: string }

MK2Events.QR_EXPIRED

Fired when the QR code expires (client-side timeout).

Detail: { challengeId: string }

MK2Events.QR_REFRESHED

Fired when a QR code is refreshed (manually via session.refresh() or automatically before expiration).

Detail: { qrUrl: string, challengeId: string }

Passkey Management Events

MK2Events.PASSKEY_ADDED

Fired when a new passkey is successfully registered.

Detail: { passkeyId: string }

MK2Events.PASSKEY_DELETED

Fired when a passkey is deleted.

Detail: { passkeyId: string }

MK2Events.PASSKEYS_LOADED

Fired when the passkey list is fetched.

Detail: { count: number }

Event Flow Diagrams

Passkey Authentication

User clicks "Sign in"
   |
auth.passkey.authenticate()
   |
PASSKEY_START -----> Update UI (show spinner, disable button)
   |
Browser WebAuthn dialog
   |
   +--[user completes]----> PASSKEY_SUCCESS --> AUTH_SUCCESS --> redirect
   |
   +--[user dismisses]----> PASSKEY_ERROR (code: user_cancelled)
   |                        AUTH_ERROR (code: user_cancelled)
   |                        --> reset UI silently
   |
   +--[no credential]-----> PASSKEY_ERROR (code: credential_not_found)
                            AUTH_ERROR --> show "register a passkey" prompt

QR Code Authentication

User clicks "QR Login"
   |
auth.qr.startAuth()
   |
QR_GENERATED -----------> Display QR image
   |
WebSocket connects (falls back to polling after 2 min)
   |
QR_STATUS_CHANGE --------> Update status text
   |
   +--[mobile scans]-------> QR_STATUS_CHANGE (status: scanned)
   |                          --> show "completing on phone..."
   |
   |  +--[mobile completes]--> QR_SUCCESS --> AUTH_SUCCESS --> redirect
   |  |
   |  +--[mobile cancels]----> QR_ERROR (code: user_cancelled)
   |                           --> auto-regenerate QR (composite components)
   |
   +--[timeout]-------------> QR_EXPIRED --> show refresh prompt
   |                          (auto-refreshes 1 min before expiration)
   |
   +--[network error]-------> QR_ERROR (code: server_unreachable)
                              AUTH_ERROR --> show error

Using Error Codes in Event Handlers

The code field in error events is a MK2ErrorCode value. Use it to drive differentiated UX:

import { MK2Events, MK2ErrorCode } from './masterkey2.js';

auth.on(MK2Events.AUTH_ERROR, (e) => {
  switch (e.detail.code) {
    case 'user_cancelled':
      // User dismissed dialog -- reset UI silently, no error message
      resetButton();
      break;

    case 'server_unreachable':
      showBanner('Cannot reach authentication server. Check your connection.');
      break;

    case 'credential_not_found':
      showBanner('No passkey found for this account. Register one first.');
      break;

    case 'user_disabled':
      showBanner('Your account has been suspended. Contact support.');
      break;

    case 'challenge_expired':
      // QR_EXPIRED also fires for this case
      break;

    default:
      showBanner('Authentication failed: ' + e.detail.error.message);
  }
});

Complete Examples

Example 1: Simple Login with Global Events

import MasterKey2, { MK2Events } from './masterkey2.js';

const auth = new MasterKey2({
  apiBaseUrl: 'https://auth.example.com'
});

// One handler for ALL auth methods
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
  window.location.href = e.detail.redirectUrl || '/';
});

// One error handler for ALL auth methods
auth.on(MK2Events.AUTH_ERROR, (e) => {
  if (e.detail.code === 'user_cancelled') return;
  document.getElementById('error').textContent = e.detail.error.message;
});

// Button click -- NO callbacks needed
document.getElementById('login-btn').onclick = () => {
  auth.passkey.authenticate(); // Events handle the rest
};

Example 2: QR Code with Status Updates

const qrImg = document.getElementById('qr-img');
const statusEl = document.getElementById('status');

auth.on(MK2Events.QR_GENERATED, (e) => {
  qrImg.src = e.detail.qrUrl;
  qrImg.style.display = 'block';
  statusEl.textContent = 'Scan the QR code with your phone';
});

auth.on(MK2Events.QR_STATUS_CHANGE, (e) => {
  if (e.detail.status === 'scanned') {
    statusEl.textContent = 'QR scanned! Complete authentication on your phone...';
    qrImg.style.opacity = '0.3';
  }
});

auth.on(MK2Events.QR_REFRESHED, (e) => {
  qrImg.src = e.detail.qrUrl;
  qrImg.style.opacity = '1';
  statusEl.textContent = 'Scan the new QR code';
});

auth.on(MK2Events.QR_EXPIRED, () => {
  statusEl.textContent = 'QR expired. Generating a new one...';
});

auth.on(MK2Events.QR_SUCCESS, (e) => {
  statusEl.textContent = 'Success! Redirecting...';
});

// Start QR auth -- events handle everything
document.getElementById('qr-btn').onclick = () => {
  auth.qr.startAuth();
};

Example 3: Multiple Components Listening

A key strength of events: multiple independent components can react to the same event without knowing about each other.

// Component 1: Status indicator
class StatusIndicator {
  constructor(auth) {
    this.el = document.getElementById('status-indicator');

    auth.on(MK2Events.AUTH_SUCCESS, () => {
      this.el.textContent = 'Authenticated';
      this.el.className = 'success';
    });

    auth.on(MK2Events.AUTH_ERROR, (e) => {
      if (e.detail.code === 'user_cancelled') return;
      this.el.textContent = e.detail.error.message;
      this.el.className = 'error';
    });
  }
}

// Component 2: Loading spinner
class LoadingSpinner {
  constructor(auth) {
    this.el = document.getElementById('spinner');

    auth.on(MK2Events.PASSKEY_START, () => {
      this.el.style.display = 'block';
    });

    // Hide on any terminal state
    auth.on(MK2Events.PASSKEY_SUCCESS, () => this.hide());
    auth.on(MK2Events.PASSKEY_ERROR, () => this.hide());
  }

  hide() { this.el.style.display = 'none'; }
}

// Component 3: Analytics tracker
class AnalyticsTracker {
  constructor(auth) {
    auth.on(MK2Events.AUTH_SUCCESS, (e) => {
      analytics.track('auth_success', { user: e.detail.user?.external_id });
    });

    auth.on(MK2Events.AUTH_ERROR, (e) => {
      if (e.detail.code === 'user_cancelled') return; // Don't track dismissals
      analytics.track('auth_error', { code: e.detail.code });
    });
  }
}

// Initialize -- each component subscribes independently
const auth = new MasterKey2({ apiBaseUrl: '...' });
new StatusIndicator(auth);
new LoadingSpinner(auth);
new AnalyticsTracker(auth);

// One trigger, all three components respond
document.getElementById('login-btn').onclick = () => auth.passkey.authenticate();

Example 4: React Integration

import { useEffect, useState, useRef } from 'react';
import MasterKey2, { MK2Events } from './masterkey2.js';

function useAuth(apiBaseUrl) {
  const authRef = useRef(null);
  if (!authRef.current) {
    authRef.current = new MasterKey2({ apiBaseUrl });
  }
  return authRef.current;
}

function LoginPage() {
  const auth = useAuth('https://auth.example.com');
  const [status, setStatus] = useState('');
  const [error, setError] = useState('');
  const [qrUrl, setQrUrl] = useState('');

  useEffect(() => {
    const onSuccess = (e) => {
      window.location.href = e.detail.redirectUrl || '/';
    };
    const onError = (e) => {
      if (e.detail.code === 'user_cancelled') {
        setStatus('');
        return;
      }
      setError(e.detail.error.message);
    };
    const onQR = (e) => {
      setQrUrl(e.detail.qrUrl);
      setStatus('Scan the QR code');
    };
    const onQRStatus = (e) => {
      if (e.detail.status === 'scanned') {
        setStatus('QR scanned! Completing...');
      }
    };

    auth.on(MK2Events.AUTH_SUCCESS, onSuccess);
    auth.on(MK2Events.AUTH_ERROR, onError);
    auth.on(MK2Events.QR_GENERATED, onQR);
    auth.on(MK2Events.QR_STATUS_CHANGE, onQRStatus);

    // Cleanup on unmount
    return () => {
      auth.off(MK2Events.AUTH_SUCCESS, onSuccess);
      auth.off(MK2Events.AUTH_ERROR, onError);
      auth.off(MK2Events.QR_GENERATED, onQR);
      auth.off(MK2Events.QR_STATUS_CHANGE, onQRStatus);
    };
  }, [auth]);

  return (
    <div>
      <button onClick={() => auth.passkey.authenticate()}>Sign in with Passkey</button>
      <button onClick={() => auth.qr.startAuth()}>Sign in with QR Code</button>
      {qrUrl && <img src={qrUrl} alt="QR Code" />}
      {status && <p>{status}</p>}
      {error && <p className="error">{error}</p>}
    </div>
  );
}

Example 5: Vue 3 Composition API

<template>
  <div>
    <button @click="loginWithPasskey">Sign in with Passkey</button>
    <button @click="loginWithQR">Sign in with QR Code</button>
    <img v-if="qrUrl" :src="qrUrl" alt="QR Code" />
    <p v-if="status">{{ status }}</p>
    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import MasterKey2, { MK2Events } from './masterkey2.js';

const status = ref('');
const error = ref('');
const qrUrl = ref('');

const auth = new MasterKey2({
  apiBaseUrl: 'https://auth.example.com'
});

const handlers = {
  success: (e) => { window.location.href = e.detail.redirectUrl || '/'; },
  error: (e) => {
    if (e.detail.code === 'user_cancelled') { status.value = ''; return; }
    error.value = e.detail.error.message;
  },
  qr: (e) => { qrUrl.value = e.detail.qrUrl; status.value = 'Scan the QR code'; },
  qrStatus: (e) => {
    if (e.detail.status === 'scanned') status.value = 'QR scanned! Completing...';
  },
};

onMounted(() => {
  auth.on(MK2Events.AUTH_SUCCESS, handlers.success);
  auth.on(MK2Events.AUTH_ERROR, handlers.error);
  auth.on(MK2Events.QR_GENERATED, handlers.qr);
  auth.on(MK2Events.QR_STATUS_CHANGE, handlers.qrStatus);
});

onUnmounted(() => {
  auth.off(MK2Events.AUTH_SUCCESS, handlers.success);
  auth.off(MK2Events.AUTH_ERROR, handlers.error);
  auth.off(MK2Events.QR_GENERATED, handlers.qr);
  auth.off(MK2Events.QR_STATUS_CHANGE, handlers.qrStatus);
});

const loginWithPasskey = () => auth.passkey.authenticate();
const loginWithQR = () => auth.qr.startAuth();
</script>

Example 6: Svelte

<script>
  import { onMount, onDestroy } from 'svelte';
  import MasterKey2, { MK2Events } from './masterkey2.js';

  let status = '';
  let error = '';
  let qrUrl = '';

  const auth = new MasterKey2({
    apiBaseUrl: 'https://auth.example.com'
  });

  const onSuccess = (e) => { window.location.href = e.detail.redirectUrl || '/'; };
  const onError = (e) => {
    if (e.detail.code === 'user_cancelled') { status = ''; return; }
    error = e.detail.error.message;
  };
  const onQR = (e) => { qrUrl = e.detail.qrUrl; status = 'Scan the QR code'; };
  const onQRStatus = (e) => {
    if (e.detail.status === 'scanned') status = 'QR scanned! Completing...';
  };

  onMount(() => {
    auth.on(MK2Events.AUTH_SUCCESS, onSuccess);
    auth.on(MK2Events.AUTH_ERROR, onError);
    auth.on(MK2Events.QR_GENERATED, onQR);
    auth.on(MK2Events.QR_STATUS_CHANGE, onQRStatus);
  });

  onDestroy(() => {
    auth.off(MK2Events.AUTH_SUCCESS, onSuccess);
    auth.off(MK2Events.AUTH_ERROR, onError);
    auth.off(MK2Events.QR_GENERATED, onQR);
    auth.off(MK2Events.QR_STATUS_CHANGE, onQRStatus);
  });
</script>

<button on:click={() => auth.passkey.authenticate()}>Sign in with Passkey</button>
<button on:click={() => auth.qr.startAuth()}>Sign in with QR Code</button>
{#if qrUrl}<img src={qrUrl} alt="QR Code" />{/if}
{#if status}<p>{status}</p>{/if}
{#if error}<p class="error">{error}</p>{/if}

Best Practices

1. Use Global Events for Common Actions

// One handler for all auth methods
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
  window.location.href = e.detail.redirectUrl || '/';
});

// Don't duplicate for each method:
// auth.on(MK2Events.PASSKEY_SUCCESS, ...) // redundant
// auth.on(MK2Events.QR_SUCCESS, ...)      // redundant

2. Always Handle user_cancelled

Users dismissing the WebAuthn dialog is normal, not an error. Don’t show error UI for it:

auth.on(MK2Events.AUTH_ERROR, (e) => {
  if (e.detail.code === 'user_cancelled') {
    resetUI(); // Silently reset
    return;
  }
  showError(e.detail.error.message);
});

3. Clean Up Listeners in SPAs

Prevent memory leaks by removing listeners when components unmount:

// React
useEffect(() => {
  const handler = (e) => { /* ... */ };
  auth.on(MK2Events.AUTH_SUCCESS, handler);
  return () => auth.off(MK2Events.AUTH_SUCCESS, handler);
}, []);

// Vue
onMounted(() => auth.on(MK2Events.AUTH_SUCCESS, handler));
onUnmounted(() => auth.off(MK2Events.AUTH_SUCCESS, handler));

// Svelte
onMount(() => auth.on(MK2Events.AUTH_SUCCESS, handler));
onDestroy(() => auth.off(MK2Events.AUTH_SUCCESS, handler));

4. Use once() for One-Shot Analytics

auth.once(MK2Events.AUTH_SUCCESS, (e) => {
  analytics.track('first_login', {
    method: 'passkey',
    user: e.detail.user?.external_id
  });
});

5. Use Specific Events for Custom Behavior

Global events handle the common case. Specific events let you add targeted behavior:

// Specific: play a sound when QR is scanned
auth.on(MK2Events.QR_STATUS_CHANGE, (e) => {
  if (e.detail.status === 'scanned') {
    playSound('beep');
  }
});

// Global: redirect on any auth success
auth.on(MK2Events.AUTH_SUCCESS, (e) => {
  window.location.href = e.detail.redirectUrl || '/';
});

6. Combine with Composite Components

Events from the JS SDK and DOM events from composite components are independent. You can use both:

<masterkey2-authenticate api-base-url="https://auth.example.com"></masterkey2-authenticate>

<script type="module">
import MasterKey2, { MK2Events } from './masterkey2.js';

// Component handles UI rendering and browser detection
// JS events add cross-cutting concerns
const auth = new MasterKey2({ apiBaseUrl: 'https://auth.example.com' });

auth.on(MK2Events.AUTH_SUCCESS, (e) => {
  analytics.track('login', { user: e.detail.user?.external_id });
});

// DOM event from the component for redirect
document.querySelector('masterkey2-authenticate')
  .addEventListener('success', (e) => {
    window.location.href = e.detail.redirectUrl || '/';
  });
</script>

Debugging Events

Enable Debug Mode

const auth = new MasterKey2({
  apiBaseUrl: 'https://auth.example.com',
  debug: true  // All events logged as [MasterKey2 SDK] Event: mk2:... { ... }
});

Log All Events

Object.values(MK2Events).forEach(eventName => {
  auth.on(eventName, (e) => {
    console.log(`[${eventName}]`, e.detail);
  });
});

Event Monitor Component

Build a live event log for development:

class EventMonitor {
  constructor(auth, containerEl) {
    Object.values(MK2Events).forEach(eventName => {
      auth.on(eventName, (e) => {
        const entry = document.createElement('div');
        entry.textContent = `${new Date().toISOString()} ${eventName}`;
        entry.title = JSON.stringify(e.detail, null, 2);
        containerEl.prepend(entry);
      });
    });
  }
}

// Usage:
new EventMonitor(auth, document.getElementById('event-log'));

Choosing an Integration Approach

CallbacksWeb ComponentsEvents
CouplingMediumLowVery Low
SetupPass options to methodAdd HTML elementRegister listeners once
Multiple listenersNoVia DOM eventsYes
Framework fitAnyServer-rendered, vanilla JSReact, Vue, Svelte, Angular
Browser detectionManualAutomatic (composite)Manual
Best forSimple integrationsDrop-in UIComplex apps, analytics

All three approaches work together and fire simultaneously — choose what fits your architecture.

Real-World References

  • bv-masterkey2 admin login (templates/pages/admin/login.html) — same-origin integration using <masterkey2-authenticate> with a DOM success event listener that calls POST /admin/passkey-login to create an admin session.
  • bv-masterkey2-web (bv-masterkey2-web/src/components/WebAuthnLogin.svelte) — cross-origin Svelte wrapper using DOM events from the composite component with differentiated error handling per MK2ErrorCode.