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

@oneaddress/partner-sdk

v1.1.1

Published

Official OneAddress partner integration SDK — webhook verification, address decryption, LOA validation

Downloads

337

Readme

@oneaddress/partner-sdk

Official Node.js SDK for OneAddress partner webhook integrations.

Handles signature verification, replay-window enforcement, ECDH address decryption, and secret-rotation grace windows in a single call.


Installation

npm install @oneaddress/partner-sdk

Quick start

import { createOneAddressHandler } from '@oneaddress/partner-sdk';

export const handler = createOneAddressHandler({
  webhookSecret: process.env.OA_WEBHOOK_SECRET!,
  privateKeyPem: process.env.OA_PRIVATE_KEY_PEM!,

  async onUpdate(event) {
    // event.address is already decrypted
    console.log(`Address updated for ${event.customer.email}:`, event.address);
    await db.updateAddress({ email: event.customer.email, ...event.address });
  },
});

// Next.js App Router
export { handler as POST };

// Express (requires express.raw middleware on this route)
// app.post('/webhook/oneaddress', express.raw({ type: 'application/json' }), handler);

Idempotency — handling retries

If your handler takes longer than ~10 seconds OneAddress will time out and retry the webhook. Your onUpdate can be called more than once for the same address change. Use event.dispatchId (from the X-OneAddress-Dispatch header) as an idempotency key:

async onUpdate(event) {
  if (event.dispatchId) {
    const seen = await db.dispatches.exists({ dispatchId: event.dispatchId });
    if (seen) return; // idempotent no-op
  }

  await db.updateAddress({ email: event.customer.email, ...event.address });

  if (event.dispatchId) {
    await db.dispatches.insert({ dispatchId: event.dispatchId });
  }
}

dispatchId falls back to '' (empty string) if the header is absent — always guard before using it as a key.

At-least-once delivery: The dispatch ID is recorded after onUpdate succeeds. This means if your process crashes between a successful onUpdate and the ID being stored, the same event will be delivered again on retry. This is intentional — the alternative (recording before processing) risks silently dropping updates if onUpdate fails.

Recommendation: Make your database write idempotent (e.g. INSERT … ON CONFLICT DO UPDATE) rather than relying solely on the dispatchId check. That way a duplicate delivery is harmless even if the ID was never recorded.


Secret rotation grace window

After rotating your webhook secret in the Partner Portal, OneAddress keeps dispatching webhooks signed with the old secret for up to 24 hours to cover in-flight requests. Pass both secrets to the handler during the transition:

export const handler = createOneAddressHandler({
  webhookSecret:           process.env.OA_WEBHOOK_SECRET!,
  previousWebhookSecret:   process.env.OA_PREVIOUS_WEBHOOK_SECRET,
  webhookSecretGraceUntil: process.env.OA_WEBHOOK_SECRET_GRACE_UNTIL, // ISO-8601
  privateKeyPem:           process.env.OA_PRIVATE_KEY_PEM!,
  async onUpdate(event) { /* ... */ },
});

Once webhookSecretGraceUntil passes, the previous secret is ignored automatically. You can then remove those two env vars.

Standalone verifyWithGrace

If you're not using the handler factory, use verifyWithGrace directly instead of calling verifySignature twice:

import { verifyWithGrace, isFreshTimestamp } from '@oneaddress/partner-sdk';

// Replay check first — before signature verify
if (!isFreshTimestamp(req.headers['x-oneaddress-timestamp'])) {
  return res.status(400).json({ error: 'Stale timestamp' });
}

const ok = verifyWithGrace(
  rawBody,
  req.headers['x-oneaddress-timestamp'],
  req.headers['x-oneaddress-signature'],
  process.env.OA_WEBHOOK_SECRET!,
  process.env.OA_PREVIOUS_WEBHOOK_SECRET,
  process.env.OA_WEBHOOK_SECRET_GRACE_UNTIL,
);
if (!ok) return res.status(401).json({ error: 'Invalid signature' });

Address verification events

Implement onVerify to respond to consumer address-match checks:

export const handler = createOneAddressHandler({
  webhookSecret: process.env.OA_WEBHOOK_SECRET!,
  privateKeyPem: process.env.OA_PRIVATE_KEY_PEM!,

  async onUpdate(event) { /* ... */ },

  async onVerify(event) {
    const stored = await db.getAddress({ email: event.customer.email });
    const match  = stored?.street === event.address.street &&
                   stored?.postcode === event.address.postcode;
    return { match, note: match ? undefined : 'Address does not match our records' };
  },
});

Edge runtime (Cloudflare Workers, Next.js Edge)

import { createOneAddressHandler } from '@oneaddress/partner-sdk/edge';

All exports are available on the /edge path. No Node.js-specific APIs are used.


Standalone utilities

import {
  verifySignature,     // single-secret HMAC verify
  verifyWithGrace,     // two-secret grace-window verify
  decryptAddress,      // ECDH + HKDF + AES-GCM decryption
  transformAddress,    // decrypt then re-encrypt to a different public key (e.g. KMS)
  isFreshTimestamp,    // ±5-minute replay-window check
  CURRENT_VERSION,     // '2026.1'
  SUPPORTED_VERSIONS,  // ['2026.1']
} from '@oneaddress/partner-sdk';

verifySignature(rawBody, signature, timestamp, secret)

Returns true if the HMAC-SHA256 signature is valid. Signing formula: HMAC-SHA256("${timestamp}.${rawBody}").

isFreshTimestamp(timestamp, toleranceSecs?)

Returns true if timestamp (Unix seconds string) is within toleranceSecs of now. Default tolerance: 300 seconds (±5 minutes). Always check freshness before signature — a stale-timestamp check is cheap; a signature verify is not.

decryptAddress(payload, privateKeyPem, partnerId)

Decrypts the address_encrypted blob using your PKCS#8 ECDH P-256 private key.

transformAddress(payload, privateKeyPem, partnerId, recipientSpkiPem)

Decrypts a OneAddress payload and immediately re-encrypts it to a different ECDH P-256 public key — for example, your internal KMS or HSM. The plaintext address is held in memory only for the duration of the call and is never returned to the caller.

import { transformAddress } from '@oneaddress/partner-sdk';

async onUpdate(event) {
  // Re-encrypt to your KMS key — plaintext never leaves this function
  const kmsBlob = await transformAddress(
    event.raw.address_encrypted as EncryptedPayload,
    process.env.OA_PRIVATE_KEY_PEM!,
    event.partner_id,
    process.env.KMS_PUBLIC_KEY_SPKI_PEM!,  // your KMS/HSM SPKI PEM public key
  );
  await db.storeEncryptedAddress(event.customer.email, kmsBlob);
}

Each call generates a fresh ephemeral key pair and random HKDF salt — full forward secrecy, no shared state between calls.


Protocol versioning

The SDK warns (but does not reject) if it receives a protocol version it doesn't recognise:

[OneAddress SDK] Unrecognised protocol version "2027.1".
Supported: 2026.1. Update @oneaddress/partner-sdk to the latest version.

This ensures you keep receiving events while updating your SDK. See SUPPORTED_VERSIONS to check which versions the installed SDK build supports.


Testing your integration

Quick smoke test — send a single test webhook from the Partner Portal (Profile → Test Webhook).

Full conformance suite — run 10 checks against your endpoint using real crypto, no mocks:

# Against a live endpoint
npx @oneaddress/conformance test https://your-endpoint.com/webhook --secret whsec_...

# Against a local server (run from the machine where the server is running)
npx @oneaddress/conformance test http://localhost:3001/webhook

Or use the Integration Conformance card in the Partner Portal to run the same checks against your configured webhook URL from within the portal.