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

cybersource-brunei-payauth

v0.4.0

Published

Cybersource payer-authentication, payments, permanent card tokenization, refunds, and guarded card payouts client for Brunei merchants.

Readme

cybersource-brunei-payauth

Cybersource REST API client for card payments with EMV 3-D Secure payer authentication, including issuer step-up (OTP) redirect handling for Brunei-issued cards. Hand-rolled HTTP Signature auth — no Cybersource SDK dependency.

Install

Copy this package into your project (or publish it to your own npm registry) — it has zero runtime dependencies.

Usage

const { createCyberSourceClient, CyberSourceApiError } = require('cybersource-brunei-payauth');

const client = createCyberSourceClient({
  merchantId: process.env.CYBERSOURCE_MERCHANT_ID,
  apiKeyId: process.env.CYBERSOURCE_API_KEY_ID,
  apiKeySecret: process.env.CYBERSOURCE_API_KEY_SECRET,
  environment: 'sandbox', // or 'production'
  // Payouts remain disabled unless the guarded configuration documented below is supplied.
});

async function main() {
  // Illustrative placeholder values — replace with real order/customer data.
  const card = { number: '4111111111111111', type: '001', expirationMonth: '12', expirationYear: '2030' };
  const orderAmount = { currency: 'BND', totalAmount: '50.00' };
  const billTo = {
    firstName: 'Jane', lastName: 'Doe',
    address1: 'Jalan Kianggeh', locality: 'Bandar Seri Begawan',
    administrativeArea: 'BM', postalCode: 'BS8811', country: 'BN',
    email: '[email protected]', phoneNumber: '2234567',
  };

  // 1. Setup
  const setup = await client.setupPayerAuth({ paymentInstrument: card });

  // Device Data Collection (DDC) happens in the SHOPPER'S BROWSER, not here in Node — this
  // package cannot and does not attempt it. Your frontend must render a hidden iframe that
  // POSTs setup.accessToken (as the "JWT" form field) to setup.deviceDataCollectionUrl before
  // your enrollment check below runs. See "Device Data Collection" further down for the exact
  // HTML your frontend needs to render.

  // 2. Enrollment check
  const enrollment = await client.checkEnrollment({
    referenceId: setup.referenceId,
    paymentInstrument: card,
    orderAmount,
    returnUrl: 'https://your-site.example/payer-auth-return',
    // Optional — improves issuer risk scoring and can reduce step-up challenge rates. Collect
    // these from the shopper's browser (e.g. navigator.*, screen.*, an XHR for their IP) and
    // pass whichever subset you have; omitted fields are simply not sent.
    // deviceInformation: { httpBrowserLanguage: 'en-US', ipAddress: '203.0.113.5', ... },
  });

  // authenticationResult ends up populated either directly by checkEnrollment (frictionless
  // pass) or, after a step-up challenge, by validateStepUp.
  let authenticationResult = enrollment.authenticationResult;

  if (enrollment.stepUpRequired) {
    // 3. Serve this HTML — the customer is redirected to their bank's OTP page
    const html = client.buildStepUpFormHtml(enrollment.stepUp);
    // ... respond with `html` from your route handler here ...

    // 4. Your /payer-auth-return route receives the bank's POST-back (TransactionId, MD)
    const transactionId = /* req.body.TransactionId from your return-URL route handler */ undefined;
    const validation = await client.validateStepUp({
      transactionId,
      veresEnrolled: enrollment.stepUp.veresEnrolled,
      paymentInstrument: card,
      orderAmount,
    });

    if (!validation.authenticationResult) {
      // validation.failureReason explains why (e.g. wrong OTP) — ask for another payment method
      return;
    }
    authenticationResult = validation.authenticationResult;
  }

  // 5. Authorize (authenticationResult comes from checkEnrollment or validateStepUp)
  const payment = await client.authorize({ paymentInstrument: card, orderAmount, billTo, authenticationResult });
  // Optional: authorize({ ..., saveCard: true }) — see "Tokenization" below for its behavior
  // and the extra token-minting request it performs after a successful purchase.

  // 6. Capture
  const captured = await client.capture(payment.id, { currency: 'BND', totalAmount: '50.00' });
}

CyberSourceApiError is thrown only for transport/HTTP-level failures (bad signature, network error, malformed response). Business outcomes (declined payment, failed OTP) are returned normally — check status/authenticationResult/failureReason on the resolved value.

Simplified flow (startPayment / finishPayment)

For the common case — one card, one purchase, capture immediately — startPayment/finishPayment collapse the manual setupPayerAuthcheckEnrollment → (buildStepUpFormHtml → persist state → validateStepUp) → authorizecapture sequence above into two calls, with no database schema of your own required for the payment lifecycle itself:

// Your /checkout route:
const result = await client.startPayment({
  paymentInstrument: card, orderAmount, billTo,
  returnUrl: 'https://your-site.example/payer-auth-return',
});

if (result.outcome === 'step_up_required') {
  // Store result.resumeToken somewhere keyed by result.transactionId (one column —
  // it's an opaque, encrypted string, safe to put in a text/varchar field). Then serve
  // result.stepUpHtml — the customer is redirected to their bank's OTP page.
} else if (result.outcome === 'completed') {
  // result.paymentId, result.captureId, result.status — done.
} else {
  // result.outcome === 'declined' — result.failureReason explains why.
}

// Your /payer-auth-return route (the bank's POST-back):
const result = await client.finishPayment({
  transactionId: req.body.TransactionId, // whatever you stored resumeToken against
  resumeToken: /* look it up by transactionId */,
});
// Same three outcomes as above, plus 'authentication_failed' if the OTP itself was wrong.

What this trades away, on purpose: startPayment always captures immediately after authorizing (no delayed-capture/manual-review window), and it does not render the Device Data Collection iframe for you (see "Device Data Collection" below) — it wraps setupPayerAuth and checkEnrollment back-to-back with no pause in between, so DDC's device-fingerprinting risk-scoring benefit is skipped. If you need either of those, use the manual sequence in "Usage" above instead — startPayment/finishPayment are additive convenience, not a replacement; every function shown in "Usage" keeps working exactly as documented.

resumeToken details: encrypted (AES-256-GCM) and derived from your apiKeySecret — no new secret to configure, and Cybersource never sees it. It expires after 15 minutes (not currently configurable). Treat it the same as you'd treat a card number in transit: don't log it, and store it only as long as you need to (delete it, or let it expire, once finishPayment has consumed it).

Note that startPayment/finishPayment still go through authorize() internally, which logs full request/response bodies — see the "authorize() currently logs its full request..." bullet under "Security & PCI considerations" below.

InvalidResumeTokenError (also exported from the package root) is thrown by finishPayment if the token is malformed, was encoded with a different apiKeySecret, has been tampered with, or has expired — this is an integration-level error (wrong/stale token passed in), not a Cybersource decline, so you likely want a distinct HTTP status for it (e.g. 400) in your route's error handling, separate from CyberSourceApiError.

These two are the errors you should specifically plan for. A plain Error can still surface in rarer cases outside your control or the caller's: a malformed paymentInstrument you passed in, or (inside startPayment's step-up branch) an unusable stepUpUrl in Cybersource's own response.

Device Data Collection

setupPayerAuth's response gives you referenceId, accessToken, and deviceDataCollectionUrl — but the actual device-fingerprinting collection happens in the shopper's browser, not in this Node package (and this package will not attempt to simulate it — there is no reliable way to fingerprint a browser from a server process). Your frontend must render a hidden, zero-size iframe that auto-submits a form POSTing the accessToken to deviceDataCollectionUrl, before you call checkEnrollment:

<iframe height="10" width="10" style="display: none;" id="cardinal_collection_iframe"></iframe>
<form id="cardinal_collection_form" method="POST" target="cardinal_collection_iframe">
  <input type="hidden" name="JWT" value="{{setup.accessToken}}" />
</form>
<script>document.getElementById('cardinal_collection_form').setAttribute('action', '{{setup.deviceDataCollectionUrl}}'); document.getElementById('cardinal_collection_form').submit();</script>

Render this from your own templating/frontend (the {{...}} placeholders are yours to fill in server-side or via a small API endpoint that returns setup.accessToken/setup.deviceDataCollectionUrl to the page). Give the browser a brief moment (existing integrations typically wait ~1-2 seconds, or hook the iframe's load event) before calling checkEnrollment, so the collection request has time to complete.

3-D Secure liability shift (commerceIndicator)

Every authorize() request must tell Cybersource which authentication scheme applies (processingInformation.commerceIndicator) — Cybersource does not derive this automatically from the CAVV/ECI data in consumerAuthenticationInformation; omitting or mis-setting it risks losing the liability shift even with a fully valid 3-D Secure result. This package computes it for you automatically:

  • checkEnrollment and validateStepUp both attach a commerceIndicator to the authenticationResult they return, derived from the card's brand and the ECI value Cybersource returned. The brand is resolved from whichever source is actually available for your input shape: a raw card's own type; a Flex Microform transient token's own JWT payload (decoded locally, no network call — see "Tokenization" below); a stored card's type if you carried it forward from storeInstrument/getInstrument; or, failing all of those, whatever brand Cybersource itself echoes back on the response.
  • authorize() reads authenticationResult.commerceIndicator directly — you never need to compute or pass it yourself.
  • Confirmed mappings: Visa (vbv / vbv_attempted / vbv_failure) and Mastercard Identity Check (spa for both full and attempted authentication, spa_failure otherwise), verified against Cybersource's official per-scheme REST API field references.
  • Other schemes (Amex, JCB, Discover, Diners, etc.) fall back to Cybersource's documented default, internet, because this package does not yet have confirmed-correct mapping values for them. If you process a meaningful volume of 3-D-Secure-authenticated Amex/JCB traffic, verify the correct scheme-specific values against Cybersource's own documentation for that scheme before relying on the liability shift for those transactions — see src/commerceIndicator.js, which isolates this mapping in one small, independently tested function.

Tokenization

Every method that used to take a raw card now takes a paymentInstrument, which accepts any of three shapes:

// A raw card (unchanged from before)
{ number, type, expirationMonth, expirationYear }

// A Flex Microform transient token — generate this client-side with Cybersource's
// Flex Microform JS library; the card number never reaches your server at all.
{ transientToken }

// A stored TMS card — from storeInstrument's or getInstrument's response. Note: this
// always refers to the customer's CURRENT DEFAULT card. 3-D Secure has no way to verify
// a specific non-default stored card (see "Stored-card limitations" below). `type` is optional
// but strongly recommended — see the note on commerceIndicator below.
{ customerId, expirationMonth, expirationYear, type }

Store a card once, after a successful payment, then reuse it later without asking the shopper for their card again:

const stored = await client.storeInstrument({
  paymentInstrument: card, // or { transientToken }
  billTo,
  orderAmount: { currency: 'BND', totalAmount: '0.00' }, // minimal authorization to mint the token; not captured
});
// stored: { customerId, instrumentId, expirationMonth, expirationYear, type, ... }

// Later — pay with the stored card, through the same payer-auth flow as any other payment. Carry
// `type` forward along with the other fields — omitting it means 3-D Secure liability-shift
// mapping (commerceIndicator, see above) cannot determine the card's scheme and falls back to
// the safe-but-liability-shift-losing 'internet' default, even though Cybersource authenticated
// the transaction correctly.
const storedCard = {
  customerId: stored.customerId,
  expirationMonth: stored.expirationMonth,
  expirationYear: stored.expirationYear,
  type: stored.type,
};
const enrollment = await client.checkEnrollment({ referenceId: setup.referenceId, paymentInstrument: storedCard, orderAmount, returnUrl });
// ...continue exactly as in the Usage example above, using storedCard as paymentInstrument throughout.

storeInstrument places a real authorization

storeInstrument is not a no-op tokenization call — it places a real authorization via POST /pts/v2/payments (with capture: false, so it is never captured/settled), and it sends no consumerAuthenticationInformation at all, i.e. no 3-D Secure. This is unlike authorize(), which hard-refuses to run without an authenticationResult. Keep this in mind:

  • Always check stored.customerId && stored.instrumentId before persisting anything. A declined tokenization authorization resolves normally (it does not throw) with both customerId and instrumentId left undefined — persisting a stored card without this check will silently save a non-functional record.
  • storeInstrument does not automatically release its authorization. If you pass a non-zero amount, retain the returned payment id and call voidPayment with the authorized amount; otherwise the hold remains until the issuer expires it. Pass orderAmount: { totalAmount: '0.00', ... } unless you deliberately want a real hold.
  • A stored card's expiration can go stale relative to customerId alone. The expirationMonth/expirationYear you get back describe the specific card that was stored at that time. If the same customer has multiple stored cards and their default later changes, a later payer-auth run against customerId alone will 3-D-Secure-verify whichever card is currently default — which may no longer match the expiration you're holding.

Look up or remove a stored card:

const instrument = await client.getInstrument({ customerId: stored.customerId, instrumentId: stored.instrumentId });
// instrument.card: { type, expirationMonth, expirationYear, maskedNumber }

await client.deleteInstrument({ customerId: stored.customerId, instrumentId: stored.instrumentId });

Stored-card limitations

Cybersource's Payer Authentication API has no field for a specific non-default stored payment instrument — 3-D Secure always runs against whichever card is currently the customer's default. There is no supported way to 3-D-Secure-verify one specific saved card out of several; switching which card is the default is outside this package's scope.

authorize({ ..., saveCard: true }) — saving a new card during a real purchase

Combining processingInformation.actionList: ['TOKEN_CREATE'] with a 3-D-Secure-authenticated authorization in a single request has been tried in every actionTokenTypes combination against a real merchant account and rejected by Cybersource as generic invalid-request data every time, with no further diagnostic detail returned. authorize({ ..., saveCard: true }) therefore does not attempt that combination. Instead, when the purchase itself succeeds (status: 'AUTHORIZED'), it makes a second, independent, unauthenticated $0.00 authorization — reusing storeInstrument's own TOKEN_CREATE mechanism — to mint the permanent token immediately afterward:

const result = await client.authorize({
  paymentInstrument: card, // or { transientToken }
  orderAmount, billTo, authenticationResult,
  saveCard: true,
  customerTokenId, // optional — attach the new card to an existing TMS customer
});

// result: { ...the same fields authorize() always returns, plus:
//   tokenization: {
//     created: true,
//     customer: { id } | null,
//     paymentInstrument: { id } | null,
//     instrumentIdentifier: { id } | null,
//   }
//   // or, if the token-creation call itself failed:
//   // tokenization: { created: false, error: { reason, message } }
//   // or, if saveCard was false/omitted, or the purchase itself was declined:
//   // tokenization: { created: false }
// }

This costs one extra, uncaptured, $0.00 authorization per save — the tradeoff for using a request shape already verified to work on this account, instead of the still-rejected combined request. A tokenization failure never fails the purchase: the response's own payment fields (id, status, ...) are untouched regardless of whether the second call succeeds — check result.tokenization.created separately and decide your own UX (e.g. "payment successful, but we couldn't save your card") when it's false with an error.

Set clientReferenceCode to a value unique to the attempt; it is forwarded to both calls for traceability. Cross-retry deduplication remains the host application's responsibility because this package keeps no persistence or idempotency state.

saveCard: true for a paymentInstrument that already has a customerId (an existing stored card) is a harmless no-op — that card is already tokenized, so there is nothing new to create.

Charging a saved card

authorizeSavedCard is a convenience wrapper for the common "I already have a token, charge it" case — it runs the exact same setupPayerAuthcheckEnrollment → (issuer step-up if required) → authorize sequence as startPayment, for a stored card. A saved card does not skip 3-D Secure — Cybersource re-runs payer authentication on every payment regardless of whether the card is new or stored:

const result = await client.authorizeSavedCard({
  customerTokenId, expirationMonth, expirationYear, type, // the stored card's own fields
  orderAmount, billTo, returnUrl,
});
// Same three outcomes as startPayment: 'declined' | 'step_up_required' (with the same
// resumeToken/stepUpHtml pair, resumed via the existing finishPayment) | 'completed'.

Per "Stored-card limitations" above, Cybersource's Payer Authentication Setup/Enrollment endpoints only expose a customer-level reference — authorizeSavedCard always authenticates and charges the customer's current default instrument; a paymentInstrumentId for a specific non-default card is accepted for forward-compatibility but currently unused.

Additional card management

await client.updatePaymentInstrument({ customerId, instrumentId, card: { expirationMonth, expirationYear } });

await client.getCustomerPaymentInstruments({ customerId });
// => { instruments: [ { id, default, card: { type, expirationMonth, expirationYear, maskedNumber } }, ... ] }

await client.getCustomer({ customerId });
await client.deleteCustomer({ customerId }); // removes the customer profile and every card under it

Not implemented

  • Cybersource's dedicated /tms/v2/tokenize endpoint — it requires Message Level Encryption (JWE-encrypted request bodies against a Cybersource-issued public key), which this package does not implement. Every tokenization path above uses the TOKEN_CREATE-via-authorization mechanism instead, which needs no MLE and is already verified to work on a real account.
  • No-3DS merchant-initiated/recurring charges. authorizeSavedCard always runs full 3-D Secure. Cybersource's stored-credential/merchant-initiated-transaction indicator for skipping a fresh challenge is a different, not-yet-implemented request shape.

Voiding a payment

Cybersource splits "cancel this payment" into two different real operations depending on whether it's been captured yet, each against a different id — the original authorization's paymentId vs. the capture's own id from capture()'s response. checkTransaction tells you which stage a payment is at (and the exact amount a reversal must match); voidTransaction uses it to pick the right operation for you:

checkTransaction/voidTransaction can 404 for several minutes after a payment is created. They read from Cybersource's Transaction Search index, which is separate from (and lags behind) the Payments API — a CyberSourceApiError with status: 404 shortly after authorize()/capture() usually means the index hasn't caught up yet, not that the transaction doesn't exist. Cybersource's own guidance is to retry every 5 minutes, up to 5 times; this package rewrites that 404's message to say so, but doesn't retry for you. If you already know the payment hasn't been captured yet — the common case of cancelling immediately after checkout, before ever calling capture() — skip checkTransaction/voidTransaction entirely and call voidPayment directly with the amount from your own authorize() response; it doesn't depend on the Transaction Search index at all.

const outcome = await client.voidTransaction(paymentId, { reason: 'customer cancelled' });
// outcome: { operation: 'reversal' | 'capture-void', result: <Cybersource response>, checked: <checkTransaction's result> }
  • operation: 'reversal' — the authorization hadn't been captured yet, so this released the hold via an authorization reversal (POST /pts/v2/payments/{id}/reversals). The reversed amount is read automatically from Cybersource's own record of the authorized amount, not re-derived or guessed.
  • operation: 'capture-void' — a capture already existed, so this voided that instead (POST /pts/v2/captures/{id}/voids). Cybersource typically batches captures to the processor once a day, so this only works same-day; after that, use one of the refund methods below.

voidTransaction assumes the only follow-on transaction it will ever see against one of its own authorizations is this package's own capture() call — if Cybersource reports any related transaction, it voids that one. If your integration creates other follow-on transactions against the same authorization outside this package (e.g. from the Cybersource Business Center), call the lower-level functions yourself instead:

const status = await client.checkTransaction(transactionId);
// status: { id, status, statusDescription, applications, amountDetails: { authorizedAmount, totalAmount, currency }, relatedTransactionIds, raw }

await client.voidPayment(paymentId, { totalAmount, currency, reason, clientReferenceCode }); // pre-capture
await client.voidCapture(captureId, { clientReferenceCode });                                 // post-capture

voidPayment's totalAmount/currency must match the authorized amount from authorize()'s own response — not the amount you originally requested, which Cybersource notes can differ (e.g. partial approvals). It throws immediately, before any network call, if either is omitted, rather than sending Cybersource a request that would be declined anyway.

Refunding a settled payment

CyberSource provides two refund endpoints based on how the original payment was captured. Both methods support full or partial refunds; pass the amount to return, using the original transaction's currency:

// Use only when authorization and capture were combined in the original POST /pts/v2/payments.
await client.refundPayment(paymentId, {
  totalAmount: '50.00',
  currency: 'BND',
  clientReferenceCode: 'refund-order-1', // optional
});

// Use when capture() was called separately. Pass capture()'s response id, not paymentId.
await client.refundCapture(captureId, {
  totalAmount: '20.00',
  currency: 'BND',
  clientReferenceCode: 'partial-refund-order-1', // optional
});

refundPayment calls POST /pts/v2/payments/{paymentId}/refunds; refundCapture calls POST /pts/v2/captures/{captureId}/refunds. Both require totalAmount and currency and return CyberSource's response unchanged, including the refund transaction id and status.

Guarded card payouts

createPayout submits an Original Credit Transaction (OCT) to POST /pts/v2/payouts. It is default-off and implements only the recipient type currently documented by Cybersource for this endpoint: a credit, debit, or prepaid card represented under paymentInformation. It does not implement bank-account payouts, and rejects recipientInformation.accountType or recipientInformation.accountId rather than treating those proposed fields as supported.

Configure the merchant-approved boundaries explicitly:

const client = createCyberSourceClient({
  merchantId: process.env.CYBERSOURCE_MERCHANT_ID,
  apiKeyId: process.env.CYBERSOURCE_API_KEY_ID,
  apiKeySecret: process.env.CYBERSOURCE_API_KEY_SECRET,
  environment: 'sandbox',
  payouts: {
    enabled: true,
    recipientType: 'card',
    allowedCurrencies: ['BND'],              // only after Baiduri confirms BND
    allowedBusinessApplicationIds: ['FD'],   // only after Baiduri confirms FD
  },
});

const payout = await client.createPayout({
  clientReferenceInformation: {
    code: 'PAYOUT-20260902-000001', // unique; persist before sending
  },
  orderInformation: {
    amountDetails: { totalAmount: '90.00', currency: 'BND' },
  },
  processingInformation: {
    businessApplicationId: 'FD',
    commerceIndicator: 'internet',
  },
  recipientInformation: {
    firstName: 'John',
    lastName: 'Doe',
    country: 'BN',
  },
  paymentInformation: {
    paymentInstrument: { id: 'CYBERSOURCE_PAYMENT_INSTRUMENT_ID' },
  },
});
// payout: { id, status: 'ACCEPTED' | 'DECLINED' | 'INVALID_REQUEST', reconciliationId, ... }

Exactly one destination must be supplied under paymentInformation: card, customer, paymentInstrument, instrumentIdentifier, or tokenizedCard. Prefer a token over a raw card number.

Production adds another guard:

payouts: {
  enabled: true,
  recipientType: 'card',
  allowedCurrencies: ['BND'],
  allowedBusinessApplicationIds: ['FD'],
  productionApprovalReference: 'BAIDURI-OR-CYBERSOURCE-APPROVAL-REFERENCE',
}

The approval must confirm that this merchant account is enabled for OCT payouts, the selected currency and BAI are permitted, and the package's existing HTTP Signature transport is accepted. Current Cybersource documentation can require JWT authentication and message-level encryption for Payouts; this package does not implement either. A reference string is a deployment safety latch, not evidence of compatibility by itself.

createPayout never retries. ACCEPTED means Cybersource accepted the request for processing, not that the recipient has received final funds. Persist the unique reference before sending, and reconcile a timeout or network error through an approved transaction-query, webhook, or operations process before deciding whether another payout is safe.

Plain-language status descriptions

Cybersource's status values (AUTHORIZED, PENDING_AUTHENTICATION, TRANSMITTED, REVERSED, ...) are payment-industry jargon that not everyone reading your logs, admin panel, or support tickets will recognize. describeStatus(status) translates any status this package's functions return into a plain-English sentence:

const { describeStatus } = require('cybersource-brunei-payauth');
// or: client.describeStatus(status) — same function, also available on the client

describeStatus('AUTHORIZED');
// "The bank approved the charge. The money is held on the customer's card, but the merchant
//  hasn't taken it yet — that happens on capture."

describeStatus('PENDING_AUTHENTICATION');
// "Waiting on the shopper to finish verifying their card (e.g. a one-time code from their
//  bank) before the charge can proceed."

It covers every documented status value across authorize(), capture(), voidPayment(), voidCapture(), refundPayment(), refundCapture(), and createPayout(). checkTransaction()'s result already includes this as statusDescription, computed from its own status field. An unrecognized status (or null/missing) returns a safe fallback sentence rather than throwing — this is a plain-language convenience, not a validator.

BIN Lookup

lookupBin looks up card metadata — brand, funding source (credit/debit), issuing bank, issuing country — from a BIN (the card's first 6-8 digits) or a full card number, via Cybersource's POST /bin/v1/binlookup. It's a read-only, non-financial call: no authorization, no hold, nothing to void.

It takes the same paymentInstrument shapes as everything else — a raw card (a bare BIN prefix like { number: '411111' } works too, you don't need a full PAN), a Flex Microform { transientToken }, or a stored { customerId }:

const info = await client.lookupBin({ number: '411111' }); // or a full card number

// info: {
//   status: 'COMPLETED',
//   cardBrand: 'VISA',
//   cardType: '001',
//   accountFundingSource: 'DEBIT', // or 'CREDIT', 'PREPAID', etc.
//   cardProduct: 'Visa Classic',
//   cardPlatform: 'CONSUMER',
//   issuer: { name: 'Some Bank', country: 'PL', binLength: '6', accountPrefix: '41111111' },
//   raw: { /* the full Cybersource response, for anything not surfaced above */ },
// }

Any field Cybersource doesn't return for a given BIN comes back as null rather than undefined, so destructuring is safe without optional chaining. This is a standalone lookup — authorize()/startPayment() do not call it automatically (that would add a network round-trip to every payment for a feature not every caller needs); call it yourself wherever you want to show the shopper their card's brand/type before or during checkout.

Security & PCI considerations

  • No secrets are ever logged. CyberSourceApiError's details carries Cybersource's own parsed response body for an HTTP-level failure, or the raw JS Error object for a network-level failure (connection refused, DNS failure, etc.) — neither path ever includes your apiKeySecret, which is used solely to compute the HTTP Signature HMAC and never appears in any header, error, or output. apiKeyId/merchantId do appear in request headers (Signature: keyid="...", v-c-merchant-id) — this is required by Cybersource's authentication scheme and is not a leak (they are identifiers, not secrets).
  • authorize() currently logs its request and response to console — the card number is masked to its last 4 digits, and card security codes and transient token JWTs are redacted. Billing PII (billTo) and the 3-D Secure consumerAuthenticationInformation fields (never the API secret) remain in plaintext server logs. If your deployment's logs aren't treated as sensitive, strip this logging or gate it behind a debug flag.
  • Reduce PCI scope by preferring tokens over raw cards. Every method that takes a paymentInstrument accepts a Flex Microform transient token ({ transientToken }) instead of a raw card — the card number never reaches your server at all when you use it, which meaningfully reduces your PCI DSS SAQ scope compared to handling raw PANs. See "Tokenization" above.
  • Tokenization never stores or logs a PAN/CVV. storeInstrument and authorize({ saveCard: true }) only ever return Cybersource-issued token ids (customerId/instrumentId/instrumentIdentifier.id) for the host application to persist — the transient token that produced them is never written to a log, a file, or any package-level state, and this package holds no database or persistence layer of its own.
  • HTTP Signature auth is hand-rolled, not delegated to Cybersource's SDK. src/httpSignature.js implements HMAC-SHA256 request signing directly against Node's built-in crypto module — no third-party dependency handles your credentials.
  • Payout payloads are highly sensitive. Never log raw payout requests. Prefer a Cybersource token in paymentInformation so a recipient PAN is not stored or transmitted by your application. This package does not implement JWT authentication or message-level encryption, which the Payouts endpoint may require for your configuration.
  • Zero runtime dependencies. There is no node_modules supply-chain surface at runtime — the entire package is Node built-ins (crypto, fetch, Buffer) plus its own source files.
  • resumeToken (from startPayment) is encrypted, not just signed. It carries the raw card number/billing PII needed to resume a payment after a 3-D Secure challenge — AES-256-GCM, keyed via HKDF off your apiKeySecret (no new secret to configure). Store it only as long as needed and never log it, the same as you would a card number.
  • Most methods validate path-interpolated ids, not complete payloads. capture, getInstrument, deleteInstrument, and storeInstrument reject unsafe paymentId/customerId/instrumentId values before making a network call. createPayout is deliberately stricter because it moves funds: it enforces its opt-in configuration, allowlists, reference, amount, OCT indicator, and destination shape. Processor-specific fields still remain Cybersource's responsibility and can produce a normal 4xx response.