npm.io
0.2.1 • Published 23h ago

@lanbox/client

Licence
Version
0.2.1
Deps
0
Size
59 kB
Vulns
0
Weekly
0

@lanbox/client

npm version Node.js 22+ CI Zero dependencies

Typed TypeScript SDK for LanBox — local email infrastructure for developers.

LanBox captures SMTP email locally, provides an interactive browser inbox, and exposes event-driven REST APIs to wait for incoming mail, OTP verification codes, and magic links without polling. @lanbox/client is the zero-dependency TypeScript SDK for interacting with those APIs in unit, integration, and end-to-end test suites.


Install

npm install --save-dev @lanbox/client
pnpm add -D @lanbox/client

Prerequisites

  1. Node.js: v22.0.0 or newer (uses built-in fetch and crypto).
  2. LanBox running locally:
docker compose up -d
Service Default Address
Web UI http://127.0.0.1:8025
REST API http://127.0.0.1:8025/api/v1
SMTP Server 127.0.0.1:1025
Virtual Domain lanbox.test

Configure your application under test to deliver SMTP traffic to 127.0.0.1:1025 (without TLS or authentication).


Quickstart: The Race-Safe Test Pattern

In automated tests, email delivery is asynchronous. To eliminate race conditions and flaky tests, register the wait promise before triggering the application action that sends the email:

import { test, expect } from 'vitest';
import { LanBox } from '@lanbox/client';

test('user signup and email OTP verification', async () => {
  const lanbox = new LanBox();

  // 1. Generate a collision-resistant address locally (e.g. signup-a1b2c3d4e5f6@lanbox.test)
  const email = lanbox.generateAddress('signup');

  // 2. Register the waiter BEFORE triggering signup
  const otpPromise = lanbox.waitForOtp(email, { timeout: 10_000 });

  // 3. Trigger your application's signup flow
  await app.signUp({ email, password: 'SecretPassword123' });

  // 4. Await extracted OTP and complete verification
  const { otp } = await otpPromise;
  const result = await app.verifyOtp({ email, otp });

  expect(result.status).toBe('verified');
});

Virtual Addresses: LanBox does not require mailbox pre-provisioning. Any arbitrary address ending in @lanbox.test (or your configured domain) is accepted and indexed dynamically on receipt.


OTP Workflows

Extract 4–8 digit verification codes from incoming plain text or HTML emails.

Wait for Incoming OTP

Use waitForOtp() when the email is expected to arrive during the test:

const { otp, messageId, subject, receivedAt } = await lanbox.waitForOtp(
  'user@lanbox.test',
  {
    timeout: 10_000,
    length: 6,                     // Optional: enforce exact digit length
    subject: 'Verification Code',  // Optional: filter by subject substring
  }
);
Extract OTP from Stored Email

Use otp() when the email has already been received:

const { otp } = await lanbox.otp('user@lanbox.test', { length: 6 });

If no OTP is found or multiple conflicting codes appear without clear context, the promise rejects with LanBoxOtpError (OTP_NOT_FOUND or OTP_AMBIGUOUS).


Isolated Test Sessions

When running parallel test suites (e.g. multi-worker Playwright or Vitest runs), use sessions to avoid cross-test message collisions.

import { LanBox } from '@lanbox/client';

const lanbox = new LanBox();

// Create an isolated session (TTL between 60s and 7 days; defaults to 1 hour)
const session = await lanbox.createSession({ ttlSeconds: 900 });

try {
  // Generates scoped plus-address: e.g. reset+s_1234567890abcdef12345678@lanbox.test
  const email = session.address('reset');

  // Register session-scoped waiter
  const linkPromise = session.waitForLink('reset', {
    host: 'app.example.test',
    pathIncludes: '/reset-password',
    timeout: 10_000,
  });

  // Trigger password reset in your app
  await app.requestPasswordReset(email);

  const { link } = await linkPromise;
  // Use `link` in your browser automation test...
} finally {
  // Purges session metadata and all emails delivered to this session
  await session.delete();
}

Extract HTTP/HTTPS action links from captured emails.

const { link, host, pathname } = await lanbox.waitForLink(
  'user@lanbox.test',
  {
    host: 'app.example.test',          // Optional: filter by target host
    pathIncludes: '/auth/magic-login', // Optional: filter by path fragment
    timeout: 10_000,
  }
);

Security Guarantee: LanBox parses and ranks links based on context and query parameters, but never fetches, opens, or executes extracted URLs. If multiple top-ranked candidates match equally, the request rejects with LanBoxLinkError (LINK_AMBIGUOUS).


Waiting for Messages

Wait for a full RFC822 parsed message:

const message = await lanbox.wait('billing@lanbox.test', {
  from: 'receipts@example.test',
  subject: 'Invoice #2026-001',
  after: '2026-08-21T00:00:00.000Z',
  timeout: 15_000,
});

console.log(message.subject);
console.log(message.textBody);
console.log(message.htmlBody);
console.log(message.attachments);
Assertion Helpers

The SDK provides assertion aliases that throw a LanBoxAssertionError on mismatch:

// Wait and assert exact OTP match
await lanbox.toContainOtp('signup@lanbox.test', '849201', { timeout: 10_000 });

// Wait and assert link pattern match
await lanbox.toContainLink('user@lanbox.test', /\/verify\?token=[a-f0-9]+/, {
  timeout: 10_000,
});

Cancellation and Timeouts

Pass an AbortSignal to cancel pending operations when test runners enforce their own test deadlines:

const controller = new AbortController();

const waitPromise = lanbox.wait('user@lanbox.test', {
  timeout: 30_000,
  signal: controller.signal,
});

// Cancel if needed
controller.abort();

try {
  await waitPromise;
} catch (error) {
  if (error instanceof LanBoxAbortError) {
    console.log('Wait operation was aborted.');
  }
}
  • Aborted requests reject with LanBoxAbortError.
  • Server timeouts reject with LanBoxTimeoutError.

Custom Configuration

Configure remote or LAN instances:

const lanbox = new LanBox({
  baseUrl: 'http://192.168.1.20:8025/api/v1',
  domain: 'mail.example.test',
});
  • baseUrl: Base URL of the LanBox REST API (defaults to http://127.0.0.1:8025/api/v1).
  • domain: Virtual domain for generateAddress() (must match server's SMTP_DOMAIN).
  • fetch: Custom fetch implementation (defaults to globalThis.fetch).

Error Handling

All SDK errors inherit from LanBoxError and include structured properties (code, message, status, details):

Error Class Error Code Description
LanBoxTimeoutError WAIT_TIMEOUT Wait deadline expired before matching mail arrived.
LanBoxAbortError WAIT_CANCELLED Client aborted the request via AbortSignal.
LanBoxOtpError OTP_NOT_FOUND / OTP_AMBIGUOUS OTP code missing or conflicting numbers detected.
LanBoxLinkError LINK_NOT_FOUND / LINK_AMBIGUOUS Link missing or multiple ambiguous candidates found.
LanBoxHttpError HTTP_* Server returned a 4xx or 5xx response.
LanBoxAssertionError ASSERTION_FAILED Assertion alias mismatch or confirmation failure.
import { LanBox, LanBoxTimeoutError, LanBoxOtpError } from '@lanbox/client';

try {
  await lanbox.waitForOtp('user@lanbox.test', { timeout: 5_000 });
} catch (err) {
  if (err instanceof LanBoxTimeoutError) {
    console.error('Email did not arrive within 5 seconds.');
  } else if (err instanceof LanBoxOtpError) {
    console.error('Email arrived but OTP could not be determined:', err.details);
  }
}

Method Reference

Method Return Type Description
status(signal?) Promise<HealthStatus> Get runtime health of API, DB, and SMTP.
generateAddress(prefix?) string Generate collision-resistant virtual address.
latest(to, options?) Promise<Message> Get newest matching message already in DB.
wait(to, options?) Promise<Message> Wait for matching incoming message.
otp(to, options?) Promise<OtpResult> Extract OTP from latest message.
waitForOtp(to, options?) Promise<OtpResult> Wait for message and extract OTP.
link(to, options?) Promise<LinkResult> Extract magic link from latest message.
waitForLink(to, options?) Promise<LinkResult> Wait for message and extract magic link.
createSession(options?) Promise<LanBoxSession> Create an isolated test session.
getSession(id) Promise<LanBoxSession> Retrieve an existing test session.
listInboxes(options?) Promise<{ inboxes, total }> List virtual inboxes with message counts.
clearInbox(address) Promise<void> Clear all deliveries for a virtual inbox.
clearAll(confirm) Promise<void> Clear all messages and attachments (requires true).

CI & Automated Test Integration

  1. Start LanBox in your CI workflow via Docker Compose.
  2. Wait for http://127.0.0.1:8025/api/v1/health before running test suites.
  3. Use unique addresses via generateAddress() or createSession() per test worker.
  4. Clean up sessions in test afterEach or finally blocks.

See working examples in the repository:



License

No open-source license is currently selected. The repository owner must choose one before external reuse or redistribution.

Keywords