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

m2m-keygen

v2.0.0

Published

Simplify Machine to Machine signature generation in a secure way

Readme

M2mKeygen

This library exists to simplify Machine to Machine signature generation and verification in a secure way. It is the TypeScript twin of the m2m_keygen Ruby gem: both implementations produce and verify byte-for-byte identical signatures under the m2m-keygen/2 scheme, and are deployed in lockstep — whichever side you run, the other end can be either language.

If you are coming from a 1.x version: the signature scheme changed in 2.0.0 and is not compatible anymore. See docs/MIGRATING.md.

Installation

Add the package to your project by executing:

$ pnpm add m2m-keygen

or with npm

$ npm install --save m2m-keygen

Requires Node >= 22. Zero runtime dependencies.

Usage

The 2 servers share the same secret key. The sender signs the request it is about to send and puts the signature (with an expiry and a nonce) in headers. The receiver generates the same signature from the request it received and compares them.

Signing a request (client)

Use createRequestSigner to build a signer once, then call signRequest for every outgoing request. It builds the query string to send and the headers to add, generating a nonce and an expiry for you.

  • verb is the HTTP verb.
  • path is the request path.
  • params is a flat params object (no nested objects/arrays of arrays). Key order doesn't matter, the lib reformats it.
  • body is optional — the raw bytes you will send.
import { createRequestSigner } from 'm2m-keygen';

const signer = createRequestSigner({ secret: 'my_secret_key' }); // algorithm defaults to sha512

const signed = signer.signRequest({
  verb: 'GET',
  path: '/orders',
  params: { since: '2026-01-01', limit: 50 },
});

signed.query; // => the query string to send
signed.headers; // => { 'X-Signature': ..., 'X-M2M-Expiry': ..., 'X-M2M-Nonce': ... }

After signing, send signed.query and signed.headers alongside your request. With the native fetch:

const url = `https://api.example.com/orders${signed.query ? `?${signed.query}` : ''}`;

const response = await fetch(url, {
  headers: signed.headers,
});

The body is opaque: this library never serializes it for you. For JSON, stringify it yourself and set Content-Type:

const body = JSON.stringify(payload);
const signed = signer.signRequest({ verb: 'POST', path: '/orders', body });

await fetch('https://api.example.com/orders', {
  method: 'POST',
  headers: { ...signed.headers, 'Content-Type': 'application/json' },
  body,
});

generateFetcher is a shortcut that wraps this recipe around any fetch-compatible function:

import { generateFetcher } from 'm2m-keygen';

const signedFetch = generateFetcher({
  fetcher: fetch,
  secret: 'my_secret_key',
});

await signedFetch('https://api.example.com/orders', { since: '2026-01-01' });

Validating a request (server)

Use createValidator to build a validator once (e.g. at startup), then call validateRequest for every incoming request. It checks:

  • Signature matching.
  • That expiry (in the X-M2M-Expiry header) is present and within the acceptance window.
  • That nonce (in the X-M2M-Nonce header) has never been seen before, so the request can't be replayed.
import { createValidator, MemoryNonceStore } from 'm2m-keygen';

const validator = createValidator({
  secret: 'my_secret_key',
  nonceStore: new MemoryNonceStore(), // required, see below
});

Example with Express — pass the raw path/query straight from req.url (never re-parsed through URL/URLSearchParams) and the raw body bytes:

app.post('/orders', express.raw({ type: '*/*' }), async (req, res) => {
  // Split on the FIRST '?' only — a literal '?' is legal inside a query.
  const sep = req.url.indexOf('?');
  const path = sep === -1 ? req.url : req.url.slice(0, sep);
  const query = sep === -1 ? '' : req.url.slice(sep + 1);

  const isValid = await validator.validateRequest({
    verb: req.method,
    path,
    query,
    body: req.body, // a Buffer, thanks to express.raw()
    headers: req.headers,
  });

  if (!isValid) return res.sendStatus(401);
  // ...
});

If you're on a runtime that hands you a WHATWG Request (Cloudflare Workers, Deno, undici...), read path/query from new URL(request.url) and body from await request.arrayBuffer() — just make sure you're not re-deriving path/query from a value that was already re-normalized somewhere upstream (see the warning below).

The 3 signature headers

| Header | Contents | | -------------- | ------------------------------------------------- | | X-Signature | The HMAC hex digest. | | X-M2M-Expiry | Unix timestamp in seconds, as a decimal string. | | X-M2M-Nonce | A random token, used once, for replay protection. |

[!WARNING] The signed query and path must reach the server verbatim — the exact bytes that were signed. Never reconstruct the URL through URL or URLSearchParams after signing (or before validating): re-encoding, re-ordering, or re-normalizing even a single character changes the bytes the signature covers and breaks validation, even though the logical request is unchanged.

Choosing a nonce store

The nonce is what really stops replay: a captured request can't be replayed because its nonce is remembered until it expires. You have to choose a store explicitly — there is no default, so you don't end up without replay protection without knowing it.

  • MemoryNonceStore for a single-process app or local development. Careful: with several workers or several hosts, each process has its own memory, so the replay protection is only partial. Use a shared store in production.
  • A shared store (Redis, Postgres, ...) for production. Reference implementations — not part of the published package, copy and adapt them — live under examples/nonceStore/: redis.ts uses SET ... NX PX for an atomic check-and-set with a native TTL; postgres.ts uses INSERT ... ON CONFLICT DO NOTHING RETURNING plus a purgeExpired() helper to call from a cron.
  • DisabledNonceStore if you explicitly don't want replay protection (expiry-only).

Configuration options

Both createRequestSigner and createValidator accept:

  • algorithm — an HMAC digest name Node supports, default 'sha512'.
  • headerName / expiryHeader / nonceHeader — override the 3 header names, default 'X-Signature' / 'X-M2M-Expiry' / 'X-M2M-Nonce'.

createRequestSigner additionally accepts a per-call expiry/nonce override; the default expiry is now + 90s (DEFAULT_EXPIRY_TTL_SECONDS).

createValidator additionally accepts window — the acceptance window in seconds around now, strictly now < expiry < now + window, default 120.

Low-level: sign / validate

If you need to sign or validate a request without going through headers, sign/validate are the low-level primitives both helpers are built on:

import { sign, validate } from 'm2m-keygen';

const signature = sign({
  secret: 'my_secret_key',
  verb: 'get',
  path: '/orders',
  expiry: 1_700_000_000,
  nonce: 'a-nonce',
  query: 'a=1',
});

validate({
  secret: 'my_secret_key',
  signature,
  verb: 'get',
  path: '/orders',
  expiry: 1_700_000_000,
  nonce: 'a-nonce',
  query: 'a=1',
}); // => true

How does it work

This is intended for a secure discussion between 2 servers and not something in a browser as the secret key must be stored and used on both sides (and you don't want to send the secret key in the browser).

Both servers have the same secret key. The sender generates a signature matching the HTTP request it is about to send (the verb, the path, the query and the body, with an expiry and a nonce) and adds it to the request in the headers above. The receiver generates the same signature from the request it received and compares it with the received signature, in constant time.

The exact byte format — the canonical string, the length-prefixed encoding, the HMAC — is described in the Ruby gem's docs/SPEC.md, with golden vectors. That document is the cross-language contract this library reproduces exactly.

Cross-language caveat: String(1.0) === '1' in JavaScript, whereas Ruby's 1.0.to_s === '1.0'. A number param value that must sign identically on both ends should be sent as a string or bigint instead — those round-trip byte-for-byte across languages.

Development

After checking out the repo, run pnpm install to install dependencies. Then, run pnpm test to run the tests.

  • pnpm test — run the unit/integration test suite (Vitest).
  • pnpm build — build the dual ESM/CJS package into dist/.
  • pnpm typecheck / pnpm lint / pnpm coverage — the other CI gates.

To run the cross-language E2E suite against a live Ruby server locally:

cd tests/e2e/ruby && bundle install && bundle exec rackup -p 9292
M2M_RUBY_URL=http://localhost:9292 pnpm vitest run tests/e2e/ts-to-ruby.e2e.test.ts

Every commit/push is checked by husky.

Tools used in dev:

  • ESLint
  • Prettier
  • TypeScript
  • Vitest

Migration

Upgrading from 1.x? See docs/MIGRATING.md.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/zaratan/m2m_keygen_ts. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

This library is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the M2mKeygen project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.