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

@decionis/iso8583

v0.3.0

Published

ISO 8583 authorization-rail interception: match each authorization to a grant the bank holds, or evaluate it at the commit boundary, before it settles.

Readme

@decionis/iso8583

The Decionis gate on an ISO 8583 TCP wire. It sits between the switch and the issuer host, reads each authorization with the host's own profile, tokenizes the card number in the bank's process, and decides the message before the host sees it: matched to a grant the cardholder already approved, or evaluated by Decionis at the commit boundary. A refusal is answered to the client with a synthetic decline; the host never receives the message.

See it stop a transaction, in one command

npx @decionis/iso8583 demo

No account, no network, nothing to configure. It stands up a stand-in acquirer, the real interceptor and a client on loopback, then sends two authorizations, one inside the demo policy and one over it:

  + authorization    4250 minor units
      verdict           APPROVE
      acquirer received yes
      client received   0110 response code 00 (STAN 000001 echoed)

  x authorization  250000 minor units
      verdict           REJECT
      acquirer received NO, the frame never left the interceptor
      client received   0110 response code 05 (STAN 000002 echoed)

The refused authorization was never seen by the upstream. Not logged after the fact, not reversed: it did not arrive. Only the decision function is local; the framing, the packager, the tokenizer and the fail-closed path are the production ones.

Install

npm install @decionis/iso8583 @decionis/sdk

Two ways to decide

| Mode | Who decided, and when | What the interceptor does on the wire | | ---------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | match | The cardholder, before the card was submitted: the bank's AgentSafe holds the grant. | Matches the authorization to the held grant by card reference, amount, currency and merchant, claims it once, and records the host's answer as the effect. No match is answered by policy.onNoMatch. | | evaluate | Decionis, at the commit boundary: the authorization itself is the intent. | Sends the intent to enforce-and-bind. An ALLOW is claimed before the message goes to the host and finalized with the host's answer; a BLOCK is declined; an ESCALATE cannot wait for a person on this clock, so policy.onEscalate says what happens. | | shadow | Decionis, observing. | Every authorization is forwarded and every decision is recorded as a signed dossier. The mode to start in, and the only mode replay runs in. |

Run it in front of a host

npx @decionis/iso8583 serve --config ./iso8583.json
{
  "mode": "evaluate",
  "listen": {
    "host": "0.0.0.0",
    "port": 8583,
    "tls": { "certFile": "/run/secrets/listen.crt", "keyFile": "/run/secrets/listen.key" }
  },
  "upstream": {
    "host": "issuer-host.bank.internal",
    "port": 8583,
    "tls": { "caFile": "/run/secrets/issuer-ca.pem" }
  },
  "decionis": {
    "baseUrl": "https://api.decionis.com",
    "apiKeyFile": "/run/secrets/decionis-api-key",
    "tenantId": "11111111-1111-4111-8111-111111111111"
  },
  "tokenizer": { "kind": "hmac", "keyFile": "/run/secrets/card-tokenizer.key" },
  "policy": {
    "onUnavailable": "forward",
    "onEscalate": "decline",
    "onNoMatch": "decline",
    "onParseError": "forward",
    "decisionTimeoutMs": 150
  },
  "binRanges": ["411111", "522222"],
  "profile": { "name": "issuer-host", "lengthHeader": "u16be", "bitmap": "ascii" }
}

A config holds paths to secrets, never secrets: the Decionis API key, the AgentSafe operator token and the tokenizer key (32 or more bytes, base64) are read from the files named. match mode takes an agentsafe section (baseUrl, tokenFile: an operator token holding cards.authorize) in place of decionis.

The fail policy is the bank's choice, written down. onUnavailable (the authority or the runtime did not answer within decisionTimeoutMs), onEscalate (policy wants a person) and onNoMatch (no held grant) each say forward or decline. onParseError (a governed message this process cannot read with the profile) says forward, decline or close. Only corrupt framing closes a connection on its own.

Limits, all optional: maxFrameBytes, maxInFlightPerConnection, maxConnections, idleTimeoutMs, upstreamConnectTimeoutMs, responseTimeoutMs (a forwarded authorization the host never answers settles as unanswered) and gateTimeoutMs (a gate that neither answers nor fails by then is unavailable; twice the decision budget by default).

The profile lays a host's differences over the ISO 8583:1987 ASCII element table: the length header (u16be, u32be, ascii4), the bitmap encoding (binary, ascii), a TPDU, per-element layouts, which MTIs are governed, which elements carry the merchant id and the MCC, the decline and error response codes, and which elements a synthetic decline echoes. The elements that carry card data are never echoed, whatever the profile says.

Try it before it is live

npx @decionis/iso8583 simulate ./burst.json
npx @decionis/iso8583 simulate ./burst.json --config ./iso8583.json
npx @decionis/iso8583 replay ./history.jsonl --config ./iso8583.json --report ./replay.json

simulate sends a burst through the real interceptor on loopback, decided by the scenario's own policy (a ceiling and a per-card count) or by the configured gate. A scenario names cards by label; the simulator makes a test card number for each, so a scenario file never holds one:

{
  "name": "one card, five merchants",
  "policy": { "ceilingMinor": 100000, "maxPerCard": 3 },
  "authorizations": [
    { "card": "A", "amountMinor": 4250, "merchantId": "M-1", "mcc": "5812" },
    { "card": "A", "amountMinor": 1999, "merchantId": "M-2", "afterMs": 50 }
  ]
}

replay runs the bank's tokenized history through the gate offline, in shadow mode only. A record is one JSON line, { "card_token_ref", "amount_minor", "currency", "merchant_id", "mcc"?, "at", "stan"?, "rrn"? }; a record carrying anything shaped like a card number is refused before it is read. The report counts what policy would have done, the bursts per card inside the velocity window, and how many authorizations a policy would have held for the cardholder.

Embed it

import { readFileSync } from "node:fs";
import { DecionisExecutionClient } from "@decionis/sdk";
import {
  ASCII_PROFILE,
  AuthorityGate,
  HmacCardTokenizer,
  Iso8583Interceptor,
  defineProfile,
} from "@decionis/iso8583";

const profile = defineProfile({ name: "issuer-host", governedMtis: ["0100", "0200"] });
const policy = {
  onUnavailable: "forward",
  onEscalate: "decline",
  onNoMatch: "decline",
  decisionTimeoutMs: 150,
} as const;

const interceptor = new Iso8583Interceptor({
  profile,
  upstream: { host: "issuer-host.bank.internal", port: 8583 },
  tokenizer: new HmacCardTokenizer(readFileSync("/run/secrets/card-tokenizer.key")),
  gate: new AuthorityGate({
    client: new DecionisExecutionClient({
      authorityBaseUrl: "https://api.decionis.com",
      defaultHeaders: {
        authorization: `Bearer ${readFileSync("/run/secrets/decionis-api-key", "utf8").trim()}`,
      },
    }),
    tenantId: "11111111-1111-4111-8111-111111111111",
    mode: "ENFORCEMENT",
    policy,
    declineResponseCode: profile.declineResponseCode,
  }),
  policy: { ...policy, onParseError: "forward" },
  binRanges: ["411111"],
  log: (event) => console.log(JSON.stringify(event)),
});

await interceptor.listen(8583, "0.0.0.0");

MatchGate takes an AgentSafeCardClient (baseUrl, token) in place of the Decionis client. LocalGate wraps a decision function, for a shadow policy of the bank's own or a test. A gate never throws: an error is a verdict under the fail policy, with its reason.

What never leaves the process

  • The card number is read from the frame, turned into tok_<bin>_<mac> by a keyed tokenizer (the MAC as letters, so the reference is never digit-shaped), and that reference is all the gate, the intent, the log and the metrics ever see. A tokenizer that returns something shaped like a card number is refused.
  • A synthetic decline echoes only the elements the profile names, and never the PAN, track data, PIN block or EMV data.
  • The log carries ids, codes, actions and durations; the metrics are counters by action and reason, served as OpenMetrics. Nothing shaped like a card number, an amount or a merchant is written to either.
  • A replay refuses a record carrying anything shaped like a card number before it reads it.

The first release's API

createIso8583InterceptorServer, extractAsciiIso8583Metadata, buildIso8583DecisionRequest, packIso8583Frame and unpackIso8583Frames remain for integrations built on 0.1.0 that do their own field extraction. The changelog records what changed in them.

Package contents

  • ESM and CommonJS builds in dist/, with TypeScript declarations
  • the decionis-iso8583 command (demo, simulate, replay, serve)

Release

For repository-backed packaging and publish automation, use the workflow documented in docs/package-distribution.md.