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

@vennyx/mila

v0.5.2

Published

Official TypeScript/JavaScript client for the mila REST API

Readme

@vennyx/mila

Official TypeScript/JavaScript client for the mila REST API.

Requirements: Node.js only — attachment encoding relies on Buffer, so this package will not run in a browser or edge runtime as-is.

Install

npm install @vennyx/mila

Usage

import { MilaClient } from '@vennyx/mila';

const client = new MilaClient({ apiKey: process.env.MILA_API_KEY! });

const domains = await client.domains.list();

const mailbox = await client.mailboxes.create(domains[0].id, {
  local_part: 'support',
  password_method: 'invitation',
  password_recovery_email: '[email protected]',
});

await client.emails.send({
  from: mailbox.address,
  to: ['[email protected]'],
  subject: 'Hello from mila',
  text: 'Sent via @vennyx/mila.',
  attachments: [{ filename: 'invoice.pdf', content: fs.readFileSync('./invoice.pdf') }],
});

By default the client talks to https://api.mila.cx. Pass baseUrl to override it.

Works from both ESM and CommonJS:

import { MilaClient } from '@vennyx/mila';        // ESM
const { MilaClient } = require('@vennyx/mila');   // CommonJS

CommonJS support is not cosmetic: a CJS-compiled TypeScript project cannot import an ESM-only package even at the type level (TypeScript reports TS1479), which is what a Medusa plugin, and anything else not yet on ESM, builds as.

Upgrading inside a monorepo

If npm install reports "up to date" but leaves you on the old version, the lockfile is the thing holding it — not the cache. In a workspace repo the root package-lock.json pins @vennyx/mila with the previous version's resolved and integrity, and it wins over what a workspace's package.json asks for.

Measured, on a real upgrade that got stuck: --prefer-online, npm cache clean --force, deleting node_modules/@vennyx/mila, and even npm install @vennyx/mila@<version> --workspace=<name> all failed to move it. They refresh the cache or the install tree; none of them re-resolves the pinned entry.

What works is deleting the @vennyx/mila entries from the root lockfile and installing again. To check where you actually are:

npm ls @vennyx/mila                                    # every copy, and their versions
node -p "require('@vennyx/mila/package.json').version" # what your code will load

Error handling

Every non-2xx response throws a MilaApiError:

import { MilaApiError } from '@vennyx/mila';

try {
  await client.mailboxes.get(domainId, 'not-a-real-id');
} catch (err) {
  if (err instanceof MilaApiError) {
    console.error(err.status, err.message);
  }
}

Resources

account, domains, mailboxes, aliases, catchall, rewrites, identities, forwardings, emails.

Check the key before you trust it

A key from the wrong account does not look wrong. It authenticates, and client.domains.list() returns a healthy-looking list — somebody else's. Nothing fails until the first real send.

const { account, apiKey } = await client.account.get();

console.log(account.name);      // "INKLEDER" — show this and have a human confirm it
console.log(apiKey.prefix);     // "mila_ab12" — which key was pasted, never the secret
console.log(apiKey.scopes);     // ["mail:send"] — can it do what you need?
console.log(apiKey.domainAccess); // "all" | "limited"

If you are building a setup screen, show account.name. "The key is valid" means nothing to someone who is not a developer; their company's name means everything.

This needs no particular scope — whatever key the user pasted can ask, keys minted before scopes existed included.

When a send is refused because the account does not hold the sender's domain, the error carries a machine-readable code:

import { isMilaApiError } from '@vennyx/mila';

try {
  await client.emails.send({ from: '[email protected]', /* ... */ });
} catch (err) {
  if (isMilaApiError(err) && err.code === 'SENDER_DOMAIN_NOT_OWNED') {
    // wrong account's key — say so in your own words
  }
}

Use isMilaApiError(err), not err instanceof MilaApiError. This package is ESM-only, and instanceof is unreliable the moment two copies of the class exist — a CommonJS consumer, a bundler that inlined one, or two versions in the tree. The guard checks the error's shape instead, so it works in all of those.

Note for Medusa integrations: Medusa's notification module catches the provider's error and re-throws a new one carrying only the message, so code and cause are both gone before your code sees them. This was traced through a real integration — provider → workflow step → notification module — not inferred; carrying the code through your own provider and workflow step does not help, because the loss happens after both. Calling client.emails.send directly is unaffected, and so is any framework that re-throws the original error.

Adding a domain

The full self-serve lifecycle (admin-role API key required):

const domain = await client.domains.create({ name: 'example.com' }); // state: "pending"
const records = await client.domains.dnsRecords(domain.id); // what to publish, incl. the verification TXT
const checks = await client.domains.diagnostics(domain.id); // live expected-vs-found DNS check
if (checks.verification.ok && checks.mx.ok) {
  await client.domains.activate(domain.id); // server re-checks DNS itself; 422 with the failing blocks otherwise
}