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

@infinitetoken/lumber

v2.5.0

Published

TypeScript Node.js logging plugin for Lumber — AES-256-GCM encrypted, async queue, zero dependencies

Readme

@infinitetoken/lumber

Node.js client for Lumber: cloud logging with full E2E encryption and an async queue. Its one runtime dependency is @infinitetoken/lumber-kit, Lumber's shared types and constants package, which has no dependencies of its own. Encryption and delivery use Node's built-in crypto and fetch.

All log content is encrypted with AES-256-GCM before leaving the process. The API stores only the ciphertext plus each log's level (kept unencrypted for filtering and push alerts), so it cannot read your logs.

Logs are viewed in the Lumber mobile app for iOS and Android, which decrypts them on your device. The app is coming soon to the App Store.

Installation

npm install @infinitetoken/lumber

Quick start

Call lumber.config() once at app startup. Every other import in your project shares the same configured client automatically.

Warning: Calling lumber.config() a second time replaces the client immediately. Logs still queued in the previous client keep sending in the background, but lumber.flush() only waits for the new client's queue. Call await lumber.flush() before reconfiguring if you need to be sure they're delivered.

// ESM
import lumber from '@infinitetoken/lumber'

// CJS
const { default: lumber } = require('@infinitetoken/lumber')

lumber.config({
  apiKey: process.env.LUMBER_API_KEY, // from your app's Secrets screen in Lumber
})

lumber.info('Server started', { port: 3000 })
lumber.warn('Rate limit approaching', { requestId: 'req-123', metadata: { userId: '456' } })
lumber.error('Payment failed', { traceId: 'trace-abc', orderId: '789' })

// Drain the queue before process exit
await lumber.flush()

Getting your API key

Create an app in the Lumber mobile app, and it takes you straight to adding an API key. The key is shown only once, so copy it right away. Any device holding the app's key can add more from the app's Secrets screen, one per deployment if you like, each deleted on its own.

An API key is two values joined by a dot: a token, then the app's encryption key, which the app generates on your device when you create the app. The plugin sends only the token to Lumber's API and encrypts every log with the rest, so the encryption key never leaves your process. Beyond your devices and your servers, it only exists wrapped (encrypted) to the account key of each person with access to the app, so Lumber's servers can't read it. Teammates and guests you share the app with get the key automatically, end-to-end encrypted, once anyone who already has it opens Lumber.

Log methods

| Method | Level | |--------|-------| | lumber.debug(message, data?) | Debug | | lumber.info(message, data?) | Info | | lumber.log(message, data?) | Info (alias) | | lumber.warn(message, data?) | Warn | | lumber.error(message, data?) | Error | | lumber.fatal(message, data?) | Fatal | | lumber.flush(timeoutMs?) | Waits for the queue to drain: resolves true once every log is sent or dropped, false if timeoutMs passes first |

LogData

The second argument is an optional LogData object. Named fields are routed to specific parts of the encrypted payload; any other keys are merged into metadata.

lumber.info('User action', {
  requestId: 'req-abc',          // → payload.requestId
  traceId:   'trace-xyz',        // → payload.traceId
  context:   { code: 'app.ts' }, // → payload.context (merged with auto-context)
  metadata:  { version: 2 },     // → payload.metadata
  userId:    '123',              // → payload.metadata.userId (extra key)
  action:    'login',            // → payload.metadata.action (extra key)
})

Explicit metadata keys win over colliding extra keys. A context or metadata that isn't an object (a string, an array) is kept as an ordinary key in metadata, so { context: 'billing' } lands as metadata.context. The entire payload (including message, requestId, context, and metadata) is encrypted before it leaves the process.

Config

lumber.config({
  apiKey:         string,  // API key from the app's Secrets screen in Lumber (token.encryptionKey)
  endpoint?:      string,  // override API base URL (default: official Lumber API)
  timeout?:       number,  // ms to wait for Lumber to answer one log (default 10000)
  console?:       boolean | ConsoleConfig,
  context?:       boolean | ContextConfig,
})

Console mirroring

Echo logs to the terminal as they are enqueued.

// Simple: all levels, badge only
lumber.config({ ..., console: true })

// Fine-grained
lumber.config({
  ...,
  console: {
    levels:    { warn: true, error: true, fatal: true }, // omit = all levels
    prefix:    true,      // show [LEVEL] badge (default: true)
    color:     true,      // ANSI color when TTY (default: true, auto-detected)
    data:      true,      // append metadata/context as second console arg
    pretty:    true,      // pretty-print data with indentation (implies data)
    timestamp: true,      // prepend HH:MM:SS.mmm timestamp
    // timestamp: 'time'     → HH:MM:SS.mmm (same as true)
    // timestamp: 'iso'      → full ISO 8601
    // timestamp: 'relative' → +1.234s since process start
  },
})

Example output with timestamp: true:

14:32:01.000 [DEBUG] Cache miss
14:32:02.105 [INFO]  User signed in
14:32:03.820 [WARN]  Rate limit approaching
14:32:05.001 [ERROR] Payment failed
14:32:05.002 [FATAL] DB connection lost

Auto-context

Lumber captures pid, hostname, and NODE_ENV once at config() time and attaches them to every log's encrypted payload. No per-call overhead.

// Default: on
lumber.config({ ... })

// Disable entirely
lumber.config({ ..., context: false })

// Also capture file/line/column from the call site
lumber.config({ ..., context: { caller: true } })

// If lumber is wrapped in a project-level logger, shift the stack frame
lumber.config({ ..., context: { caller: true, callerDepth: 1 } })

User-supplied context in LogData is merged on top of auto-context, so you can always override specific fields per call.

Queue behavior

Logs are enqueued in memory and sent sequentially in the background, one POST /logs request per log. A network error, a 5xx response or no answer within timeout (10 seconds unless set) counts as a failed attempt: each log gets up to 5 attempts, waiting 1 s, 2 s, 4 s, then 8 s between them, and after the fifth failure it's dropped with a console warning. A log Lumber refuses (a 4xx, such as an invalid API key) is dropped at once with a warning, since sending it again won't help. When your team has sent its plan's logs for the month, Lumber answers 429: the plugin warns once and drops logs until the month turns (the response's Retry-After), rather than queueing them. Call flush() before process exit to ensure all queued logs are delivered; flush(5000) waits at most 5 seconds and resolves false if logs were still queued then. These rules, and how a log's data becomes its encrypted payload, come from @infinitetoken/lumber-kit, whose contract Lumber's Python and Go clients are tested against too.

process.on('SIGINT', async () => {
  await lumber.flush()
  process.exit(0)
})

Serverless

A serverless platform can freeze or stop a function as soon as it responds, with logs still queued. Keep the function running until they're sent with the platform's own hook, giving flush() a time limit so a slow answer can't hold the function to its maximum duration. On Vercel, with waitUntil from @vercel/functions:

import { waitUntil } from '@vercel/functions'

export default async function handler(req, res) {
  lumber.info('Handled request', { path: req.url })
  res.send('ok')
  waitUntil(lumber.flush(10_000))
}

Where there's no such hook (AWS Lambda), await lumber.flush(10_000) before returning.

Power users: multiple instances

LumberClient is exported for cases where you need more than one configured instance.

import { LumberClient } from '@infinitetoken/lumber'

const client = new LumberClient({ apiKey, console: true })
client.info('hello')
await client.flush()

Requirements

Node 18 or later (uses the built-in fetch).

License

MIT. See the LICENSE file included in the package.