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

@ontemper/as2

v0.7.1

Published

AS2 (RFC 4130) client for Temper workflows: receive, send (pack -> deliver -> confirm), MDN handling. All S/MIME crypto executes on the platform engine - the SDK implements no cryptography itself.

Readme

@ontemper/as2

AS2 (RFC 4130) client for Temper workflows. The SDK ferries protocol bytes and your AS2 configuration: identity and partner are the platform's built-in as2_identity and as2_partner Resource records, resolved by the workflow with @ontemper/resources and passed to every call. The SDK never looks up a record itself; it only defines the As2IdentityConfig/As2PartnerConfig shape (the record fields, verbatim) and sends whatever object you hand it. All crypto runs platform-side — the workflow never signs, encrypts, or verifies anything itself.

Authentication uses createAs2Client({ token: context.token }) in the fetch-handler model, then TEMPER_RUN_TOKEN, then UNNBOUND_SANDBOX_JWT. Each request sends the selected credential as Authorization: Bearer; there is no static-token fallback. Without a credential the SDK throws As2SdkError('NOT_CONFIGURED', ...).

Inbound — terminate AS2 on your workflow URL

Always terminate inbound AS2 with as2Endpoint from unnbound-events — it is the verify-then-bind seam: the platform verifies the message before any customer trace is chosen, only a verified ledger-backed trace may re-bind the request (the partner-controlled trace header is never continuation authority), and request logs pass the AS2 header allowlist so Receipt-Delivery-Option and Subject never appear. Calling receive() from a plain route skips all three.

import * as as2 from '@ontemper/as2';
import { As2PartnerResource, env } from '@ontemper/resources';
import { as2Endpoint, ServerError } from 'unnbound-events';

// One route serves every partner: the sender's AS2-From selects the partner record,
// and the resolved partner carries the identity it trades with inline. Each partner's
// AS2 ID must be unique among the records this workflow can reach; `select` refuses an
// ambiguous match. A sender with no record is a permanent rejection (the resolver answers
// 404), so the partner does not retry it; any other resolver failure stays a 500 the
// partner retries later.
const isSelectorMiss = (error: unknown): boolean => {
  for (let cause: unknown = error; cause instanceof Error; cause = cause.cause) {
    if ('status' in cause && cause.status === 404) return true;
  }
  return false;
};

server.post(
  '/as2/inbound',
  as2Endpoint(
    async ({ url, headers, body }) => {
      const as2Id = as2.unquoteAs2Id(headers['as2-from']);
      const selected = await env.resources
        .select({ resourceTypeKey: 'as2_partner', match: { as2Id } })
        .catch((error: unknown) => {
          if (isSelectorMiss(error)) throw new ServerError({ status: 403, message: 'Unknown AS2 partner' });
          throw error;
        });
      const partner = await env.resources.resolve(selected.key, As2PartnerResource);
      return as2.receive({ url, headers, body: Buffer.from(body as ArrayBuffer), partner });
    },
    async (result) => {
      if (result.kind === 'received') {
        // Delivery is at-least-once if this process stops before acknowledge(), so
        // make processing idempotent by Message-ID or a stable document key.
        await processDocument(result.payload);
        const receipt = await result.acknowledge();
        // mdnDeliveryUrl (when present) is resolved from THIS PARTNER's own inbound request
        // (their Receipt-Delivery-Option header). Nothing to configure here; just deliver as given.
        if (receipt.mdnMode === 'async' && receipt.mdn && receipt.mdnDeliveryUrl) {
          await as2.deliverMdn(receipt.mdn, receipt.mdnDeliveryUrl);
        }
        return receipt.response;
      }
      if (result.kind === 'duplicate' && result.mdnMode === 'async' && result.mdn && result.mdnDeliveryUrl) {
        await as2.deliverMdn(result.mdn, result.mdnDeliveryUrl);
      }
      // Duplicate: signed sync MDN or empty async 200. In-flight/parked: retryable 503.
      return result.response;
    },
  ),
);

Outbound — pack → deliver → confirm

const partner = await env.resources.resolve('walmart', As2PartnerResource);
const sent = await as2.send(ediDocument, {
  partner, // identity: partner.identity — pass `identity` explicitly to override
  idempotencyKey: po.number, // crash-retry re-delivers the ORIGINAL envelope
});
// sent.outcome: 'confirmed' (MIC matched) | 'failed' | 'pending' (async receipt)

Async receipts (partner.receipt === 'async'): send() returns pending and tells the partner to post the receipt to asyncReceiptUrl() — your workflow's own URL (UNNBOUND_WORKFLOW_URL) plus AS2_ASYNC_RECEIPT_PATH (/as2/mdn); pass receiptUrl to override it per call. Serve that route and settle the receipt there with as2.confirm({ partner, mdnHeaders, mdnBody }) — no message ID needed: the platform verifies the receipt first and settles the message named inside its signed content, and result.messageId tells you which outbound message that was, even with many receipts outstanding. (originalMessageId is an optional assertion — confirm fails if the signed receipt names any other message.) Delivering your own MDNs in async inbound mode is as2.deliverMdn(mdn, url) — dumb byte transport, url comes from result.mdnDeliveryUrl. The partner chooses that URL, so deliverMdn connects directly (no proxy, no redirects) and refuses a destination that is, or resolves to, a loopback, private, link-local, or metadata address. It resolves the name once per connection and dials the address it checked.

Identity & partner records

Both records are created in the Resources UI (or by the builder agent) and read with env.resources.resolve; the Zod schemas As2IdentityResource and As2PartnerResource in @ontemper/resources parse them. Private keys are always Variable references on the record; certificates may be literals or references. A rotation is an edit of the record, never a redeploy.

import type { As2CertificateSlots, As2IdentityConfig, As2KeyPairSlots, As2PartnerConfig } from '@ontemper/as2';

// As2KeyPairSlots: { certificate: string; privateKey: string; secondaryCertificate?: string | null; secondaryPrivateKey?: string | null }
// As2CertificateSlots: { certificate: string; secondaryCertificate?: string | null }

const identity: As2IdentityConfig = {
  as2Id: 'ACME-TEMPER',
  keys: { mode: 'combined', certificate: certPem, privateKey: keyPem }, // or { mode: 'separate', signing: As2KeyPairSlots, encryption: As2KeyPairSlots }
};

const partner: As2PartnerConfig = {
  as2Id: 'ACME',
  url: 'https://as2.acme.example/inbox',
  certificates: { mode: 'combined', certificate: acmeCertPem }, // or { mode: 'separate', encryption: As2CertificateSlots, signing: As2CertificateSlots }
  signOutgoing: true, // default true
  encryptOutgoing: true, // default true
  requireSignedIncoming: true, // default true
  requireEncryptedIncoming: true, // default true
  receipt: 'sync', // default 'sync'; receipts are always signed
  digestAlgorithm: 'sha-256', // default
  encryptionAlgorithm: 'aes256-cbc', // default
  identity, // inlined by `resolve`; used when a call omits `identity`
};

Outgoing messages use each group's primary slot; incoming messages and receipts are accepted with the secondary slot too, so both sides can rotate with an overlap. With separate, messages are encrypted to the partner's encryption certificate and verified with their signing certificate; our signing pair signs messages and receipts and our encryption pair decrypts. Never log the resolved identity: it carries private key material.

Observability — one customer trace per execution

Every operation emits typed as2 milestones. Send/receive milestones carry the cleartext business document and the MDN milestones carry the complete receipt (payload / mdn), so an exchange is queryable and auditable in the product — the same body-logging contract http and sftp logs already follow. Sealed envelopes, delivery tokens, certificate material, and full URLs are never logged. Internal platform calls and the raw delivery POST emit no generic HTTP logs. The platform ledger owns trace correlation:

  • Sync exchanges are one trace. send() results carry customerTraceId (the trace stored on the outbound ledger row; on idempotent replay, the original's). A verified loopback receive() recovers the sending half's trace, so both halves of a self-exchange share it.
  • Async callbacks are their own trace, linked. A delayed MDN settles on the callback's own execution trace; confirm() returns originatingTraceId and its milestone carries the link, so searching the AS2 Message-ID unifies the exchange.
  • Inbound routing is verify-then-bind. Terminate /as2/inbound with as2Endpoint from unnbound-events (the primary inbound example above): the platform verifies the message before a customer trace is chosen, then binds the request to the ledger's canonical trace. The partner-controlled trace header is never continuation authority, and request logs pass an AS2 header allowlist (Receipt-Delivery-Option and Subject are never logged).