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 setupPayerAuth → checkEnrollment → (buildStepUpFormHtml → persist state → validateStepUp) → authorize → capture 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:
checkEnrollmentandvalidateStepUpboth attach acommerceIndicatorto theauthenticationResultthey 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 owntype; a Flex Microform transient token's own JWT payload (decoded locally, no network call — see "Tokenization" below); a stored card'stypeif you carried it forward fromstoreInstrument/getInstrument; or, failing all of those, whatever brand Cybersource itself echoes back on the response.authorize()readsauthenticationResult.commerceIndicatordirectly — you never need to compute or pass it yourself.- Confirmed mappings: Visa (
vbv/vbv_attempted/vbv_failure) and Mastercard Identity Check (spafor both full and attempted authentication,spa_failureotherwise), 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 — seesrc/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.instrumentIdbefore persisting anything. A declined tokenization authorization resolves normally (it does not throw) with bothcustomerIdandinstrumentIdleftundefined— persisting a stored card without this check will silently save a non-functional record. storeInstrumentdoes not automatically release its authorization. If you pass a non-zero amount, retain the returned payment id and callvoidPaymentwith the authorized amount; otherwise the hold remains until the issuer expires it. PassorderAmount: { totalAmount: '0.00', ... }unless you deliberately want a real hold.- A stored card's expiration can go stale relative to
customerIdalone. TheexpirationMonth/expirationYearyou 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 againstcustomerIdalone 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 setupPayerAuth → checkEnrollment → (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 itNot implemented
- Cybersource's dedicated
/tms/v2/tokenizeendpoint — 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 theTOKEN_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.
authorizeSavedCardalways 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/voidTransactioncan 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 — aCyberSourceApiErrorwithstatus: 404shortly afterauthorize()/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 callingcapture()— skipcheckTransaction/voidTransactionentirely and callvoidPaymentdirectly with the amount from your ownauthorize()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-capturevoidPayment'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'sdetailscarries Cybersource's own parsed response body for an HTTP-level failure, or the raw JSErrorobject for a network-level failure (connection refused, DNS failure, etc.) — neither path ever includes yourapiKeySecret, which is used solely to compute the HTTP Signature HMAC and never appears in any header, error, or output.apiKeyId/merchantIddo 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 toconsole— 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 SecureconsumerAuthenticationInformationfields (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
paymentInstrumentaccepts 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.
storeInstrumentandauthorize({ 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.jsimplements HMAC-SHA256 request signing directly against Node's built-incryptomodule — no third-party dependency handles your credentials. - Payout payloads are highly sensitive. Never log raw payout requests. Prefer a Cybersource token in
paymentInformationso 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_modulessupply-chain surface at runtime — the entire package is Node built-ins (crypto,fetch,Buffer) plus its own source files. resumeToken(fromstartPayment) 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 yourapiKeySecret(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, andstoreInstrumentreject unsafepaymentId/customerId/instrumentIdvalues before making a network call.createPayoutis 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.
