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

@deskcreate/cybersource-3ds-web

v1.0.4

Published

CyberSource 3DS Web SDK for browser-based integrations with Flex Microform support

Readme

CyberSource 3DS Web SDK

A lightweight, framework-agnostic JavaScript SDK for handling CyberSource 3D Secure authentication and Flex Microform card collection in the browser.

Features

  • Flex Microform - PCI-compliant card collection (iFrame-based)
  • Device Data Collection - Automated fingerprinting for 3DS 2.x
  • Challenge UI - Pre-built modal for 3DS step-up authentication
  • Smart Loading - Automatic Flex SDK script loading with retry logic
  • TypeScript First - Full type definitions for a better developer experience
  • Framework Agnostic - Seamlessly integrates with React, Vue, Angular, or Vanilla JS

Installation

npm install @deskcreate/cybersource-3ds-web

🏗 Payment Flow Overview

Understanding the 3DS payment flow is crucial for proper integration:

graph TD
    A[Initialize WebClient] --> B[Get Capture Context from Backend]
    B --> C[Setup Flex Microform]
    C --> D[User Enters Card Details]
    D --> E[Tokenize Card Data]
    E --> F[Collect Browser Info]
    F --> G[Send to Backend: Start 3DS]
    G --> H{Challenge Required?}
    H -->|No - Frictionless| I[Backend Processes Payment]
    H -->|Yes - Friction| J[Show Challenge Modal]
    J --> K[User Completes Challenge]
    K --> L[Webhook Validates & Processes]
    L --> M[PostMessage to Frontend]
    M --> N[Close Modal]
    I --> O[Payment Complete]
    N --> O

Two Flow Types

1. Frictionless Flow (No Challenge)

  • Card issuer approves without additional verification
  • Backend processes payment immediately after enrollment
  • Frontend calls /complete endpoint to finalize

2. Friction Flow (Challenge Required)

  • Card issuer requires additional verification (OTP, biometric, etc.)
  • Frontend shows challenge modal (iframe)
  • User completes verification with their bank
  • Backend webhook automatically validates and processes payment
  • Webhook sends postMessage to frontend with result
  • Frontend closes modal and shows success/error

[!IMPORTANT] For friction flows, the payment is processed automatically by your backend webhook. The frontend only needs to listen for the completion message via postMessage.


Quick Start

1. HTML Setup

<!-- Container for the Flex Microform iframes -->
<div id="microform-container"></div>

<!-- Expiry Inputs (Required - Flex v2 only handles card number and CVV) -->
<input type="text" id="expiryMonth" placeholder="MM" maxlength="2" />
<input type="text" id="expiryYear" placeholder="YYYY" maxlength="4" />

Note: The SDK automatically loads the Flex Microform script when you call setupFlexMicroform. No manual script tag needed.

2. Initialize the Client

import { WebClient } from '@deskcreate/cybersource-3ds-web'

const client = new WebClient()

3. Setup Secure Card Input

[!IMPORTANT] Flex Microform only handles Card Number and CVV. You MUST collect the Expiry Month and Expiry Year using your own standard HTML inputs.

Minimal Setup

// Get capture context from your backend
const { captureContext } = await fetch('/api/capture-context').then(r => r.json())

// Setup microform
const flex = await client.setupFlexMicroform('microform-container', captureContext)

Customized Setup

const flex = await client.setupFlexMicroform('microform-container', captureContext, {
  layout: 'stacked', // or 'inline'
  placeholders: {
    cardNumber: '1234 5678 9012 3456',
    securityCode: '•••'
  },
  labels: {
    cardNumber: 'Card Number',
    securityCode: 'CVV'
  },
  customStyles: {
    fontSize: '16px',
    fontFamily: 'system-ui, sans-serif',
    textColor: '#1a1a1a',
    borderColor: '#d1d5db',
    focusBorderColor: '#4f46e5',
    borderRadius: '8px',
    inputHeight: '48px'
  }
})

4. Process Payment

// 1. Tokenize card data
const { token, cardType, maskedPan } = await flex.tokenize(expiryMonth, expiryYear)

// 2. Collect browser information for 3DS
const deviceInfo = await client.collectBrowserInfo()

// 3. Send to your backend to start 3DS enrollment
const { enrollment, transactionId } = await fetch('/api/start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    transientToken: token,
    cardType,
    amount: '100.00',
    currency: 'USD',
    deviceInformation: deviceInfo,
    customerInfo: { /* ... */ }
  })
}).then(r => r.json())

// 4. Handle challenge if required
if (enrollment.challengeRequired) {
  // Show challenge modal (non-blocking)
  client.showChallengeModal(
    enrollment.stepUpUrl,
    enrollment.accessToken,
    { transactionId }
  )
  
  // Wait for completion via postMessage
  const result = await waitForChallengeComplete(transactionId)
  
  // Close modal
  client.closeChallengeModal()
  
  if (!result.success) {
    throw new Error(result.error || 'Payment failed')
  }
  
  // Payment already completed by backend webhook!
  console.log('Payment successful:', result.payment)
  
} else {
  // Frictionless - call complete endpoint
  const payment = await fetch('/api/complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ transactionId })
  }).then(r => r.json())
  
  console.log('Payment successful:', payment)
}

📖 API Reference

WebClient

Main client class for interacting with CyberSource services.

Constructor

const client = new WebClient()

Methods

setupFlexMicroform(containerId, captureContext, options?)

Sets up secure card input fields in the specified container.

Parameters:

  • containerId (string, required) - DOM element ID where fields will be rendered
  • captureContext (string, required) - JWT from your backend's capture context API
  • options (object, optional):
    • layout?: 'stacked' | 'inline' - Field layout (default: 'stacked')
    • placeholders?: { cardNumber?: string, securityCode?: string }
    • labels?: { cardNumber?: string, securityCode?: string }
    • customStyles?: FlexFieldStyles - Custom styling options
    • onFieldChange?: (fieldType: string, data: FlexFieldChangeData) => void

Returns: Promise<FlexMicroformInstance>

Example:

const flex = await client.setupFlexMicroform('card-container', captureContext, {
  layout: 'inline',
  customStyles: {
    fontSize: '16px',
    borderRadius: '8px'
  }
})

collectBrowserInfo(ipAddress?)

Collects browser fingerprint data for 3DS authentication.

Parameters:

  • ipAddress (string, optional) - Client's public IP address

Returns: Promise<DeviceInformation>

Recommended: Use this method instead of collectDeviceData for simpler integration.

Example:

const deviceInfo = await client.collectBrowserInfo()

collectDeviceData(deviceDataUrl, accessToken, options?)

Legacy method for device data collection using hidden iframe.

Parameters:

  • deviceDataUrl (string, required) - Device data collection URL from CyberSource
  • accessToken (string, required) - Access token from CyberSource
  • options (object, optional):
    • timeoutMs?: number - Timeout in milliseconds (default: 10000)

Returns: Promise<{ success: boolean, timeout?: boolean }>


showChallengeModal(stepUpUrl, accessToken, options?)

Displays the 3DS challenge modal for user authentication.

Parameters:

  • stepUpUrl (string, required) - Challenge URL from enrollment response
  • accessToken (string, required) - Access token from enrollment response
  • options (object, optional):
    • transactionId?: string - Transaction ID for matching completion events
    • timeoutMs?: number - Modal timeout (default: 600000 / 10 minutes)

Returns: Promise<AuthenticationResult>

Note: This method is non-blocking. It shows the modal but doesn't wait for completion. Use postMessage listener to detect completion.

Example:

// Show modal (non-blocking)
client.showChallengeModal(stepUpUrl, accessToken, { transactionId })

// Listen for completion separately
window.addEventListener('message', (event) => {
  if (event.data.type === '3DS_COMPLETE' && event.data.transactionId === transactionId) {
    client.closeChallengeModal()
    // Handle result
  }
})

closeChallengeModal()

Manually closes the challenge modal.

Example:

client.closeChallengeModal()

destroy()

Cleans up all resources and removes event listeners.

Example:

client.destroy()

FlexMicroformInstance

Returned by setupFlexMicroform().

Methods

tokenize(expiryMonth, expiryYear)

Tokenizes the card data entered in the secure fields.

Parameters:

  • expiryMonth (string, required) - 2-digit month (e.g., '06')
  • expiryYear (string, required) - 4-digit year (e.g., '2029')

Returns: Promise<FlexTokenizeResult>

{
  token: string        // Transient token JWT
  maskedPan: string    // Masked card number (e.g., '411111••••••1111')
  cardType: string     // Card type (e.g., 'visa', 'mastercard')
  expiryMonth: string  // Normalized month
  expiryYear: string   // Normalized year
}

Example:

const { token, cardType, maskedPan } = await flex.tokenize('06', '2029')

isValid()

Checks if all fields are valid.

Returns: boolean


destroy()

Destroys the microform instance and cleans up.


🚀 Complete React Example

Here's a production-ready React hook implementation:

import { useState, useRef, useEffect, useCallback } from 'react'
import { WebClient, type FlexMicroformInstance } from '@deskcreate/cybersource-3ds-web'

interface PaymentCompleteMessage {
  type: '3DS_COMPLETE'
  transactionId: string
  success: boolean
  payment?: {
    id: string
    status: string
    amount: number
    currency: string
  }
  error?: string
}

export const useCheckout = () => {
  const [status, setStatus] = useState<'IDLE' | 'INITIALIZING' | 'READY' | 'PROCESSING' | 'CHALLENGING' | 'SUCCESS' | 'ERROR'>('IDLE')
  const [message, setMessage] = useState('')
  const [error, setError] = useState<Error | null>(null)
  const [result, setResult] = useState<any>(null)

  const clientRef = useRef<WebClient | null>(null)
  const microformRef = useRef<FlexMicroformInstance | null>(null)
  const challengeResolveRef = useRef<((result: PaymentCompleteMessage) => void) | null>(null)
  const currentTransactionIdRef = useRef<string | null>(null)

  // Listen for challenge completion messages
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      const data = event.data as PaymentCompleteMessage

      if (!data || data.type !== '3DS_COMPLETE') return

      // Verify transaction ID matches
      if (currentTransactionIdRef.current && data.transactionId !== currentTransactionIdRef.current) {
        return
      }

      // Resolve the challenge promise
      if (challengeResolveRef.current) {
        challengeResolveRef.current(data)
        challengeResolveRef.current = null
      }
    }

    window.addEventListener('message', handleMessage)
    return () => window.removeEventListener('message', handleMessage)
  }, [])

  // Initialize microform
  useEffect(() => {
    let isActive = true
    const client = new WebClient()
    clientRef.current = client

    const init = async () => {
      try {
        setStatus('INITIALIZING')
        setMessage('Loading...')

        // Get capture context from your backend
        const { captureContext } = await fetch('/api/capture-context').then(r => r.json())

        if (!isActive) return

        // Setup microform
        const microform = await client.setupFlexMicroform('microform-container', captureContext, {
          layout: 'stacked',
          customStyles: {
            fontSize: '16px',
            borderRadius: '8px'
          }
        })

        if (!isActive) {
          microform.destroy()
          return
        }

        microformRef.current = microform
        setStatus('READY')
        setMessage('Ready to pay')
      } catch (err: any) {
        if (!isActive) return
        setStatus('ERROR')
        setMessage(err.message || 'Failed to initialize')
        setError(err)
      }
    }

    init()

    return () => {
      isActive = false
      microformRef.current?.destroy()
      clientRef.current?.destroy()
    }
  }, [])

  // Wait for challenge completion
  const waitForChallengeComplete = useCallback((transactionId: string): Promise<PaymentCompleteMessage> => {
    return new Promise((resolve, reject) => {
      currentTransactionIdRef.current = transactionId
      challengeResolveRef.current = resolve

      // Timeout after 10 minutes
      const timeout = setTimeout(() => {
        if (challengeResolveRef.current) {
          challengeResolveRef.current = null
          reject(new Error('Challenge timed out'))
        }
      }, 600000)

      // Clear timeout on resolve
      const originalResolve = challengeResolveRef.current
      challengeResolveRef.current = (result) => {
        clearTimeout(timeout)
        originalResolve(result)
      }
    })
  }, [])

  // Payment function
  const pay = useCallback(async (amount: string, currency: string, expiryMonth: string, expiryYear: string) => {
    const microform = microformRef.current
    const client = clientRef.current

    if (!microform || !client) {
      setStatus('ERROR')
      setMessage('Payment form not ready')
      return
    }

    try {
      // 1. Tokenize
      setStatus('PROCESSING')
      setMessage('Securing card...')

      const { token, cardType } = await microform.tokenize(expiryMonth, expiryYear)

      // 2. Collect device info
      const deviceInfo = await client.collectBrowserInfo()

      // 3. Start 3DS
      setMessage('Verifying card...')

      const response = await fetch('/api/start', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          transientToken: token,
          cardType,
          amount,
          currency,
          deviceInformation: deviceInfo
        })
      }).then(r => r.json())

      const { enrollment, transactionId } = response

      // 4. Handle challenge if required
      if (enrollment.challengeRequired) {
        setStatus('CHALLENGING')
        setMessage('Verify your identity...')

        // Show modal
        client.showChallengeModal(enrollment.stepUpUrl, enrollment.accessToken, { transactionId })

        // Wait for completion
        const result = await waitForChallengeComplete(transactionId)

        // Close modal
        client.closeChallengeModal()

        if (!result.success) {
          throw new Error(result.error || 'Payment failed')
        }

        // Payment completed by backend
        setStatus('SUCCESS')
        setMessage('Payment successful!')
        setResult(result.payment)
      } else {
        // Frictionless flow
        setMessage('Processing payment...')

        const payment = await fetch('/api/complete', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ transactionId })
        }).then(r => r.json())

        setStatus('SUCCESS')
        setMessage('Payment successful!')
        setResult(payment)
      }
    } catch (err: any) {
      clientRef.current?.closeChallengeModal()
      setStatus('ERROR')
      setMessage(err.message || 'Payment failed')
      setError(err)
    }
  }, [waitForChallengeComplete])

  return {
    status,
    message,
    error,
    result,
    pay,
    isReady: status === 'READY',
    isLoading: ['INITIALIZING', 'PROCESSING', 'CHALLENGING'].includes(status)
  }
}

Usage:

function CheckoutForm() {
  const { status, message, pay, isReady } = useCheckout()
  const [expiry, setExpiry] = useState({ month: '06', year: '2029' })

  return (
    <div>
      <div id="microform-container"></div>
      
      <input 
        value={expiry.month} 
        onChange={e => setExpiry({ ...expiry, month: e.target.value })}
        placeholder="MM"
      />
      <input 
        value={expiry.year}
        onChange={e => setExpiry({ ...expiry, year: e.target.value })}
        placeholder="YYYY"
      />
      
      <button 
        onClick={() => pay('100.00', 'USD', expiry.month, expiry.year)}
        disabled={!isReady}
      >
        {status === 'PROCESSING' ? message : 'Pay $100.00'}
      </button>
    </div>
  )
}

🔧 Backend Integration

Your backend needs three endpoints:

1. Generate Capture Context

POST /api/capture-context
Body: { targetOrigins: ['https://yoursite.com'] }
Response: { captureContext: 'eyJraWQ...' }

2. Start 3DS Enrollment

POST /api/start
Body: {
  transientToken: string
  cardType: string
  amount: string
  currency: string
  deviceInformation: object
  customerInfo: object
}
Response: {
  transactionId: string
  enrollment: {
    challengeRequired: boolean
    stepUpUrl?: string
    accessToken?: string
  }
}

3. Complete Payment (Frictionless)

POST /api/complete
Body: { transactionId: string }
Response: { status: 'success', payment: {...} }

4. Webhook (Friction Flow)

POST /api/webhook/3ds?transactionId=xxx&cardType=xxx
Body: { TransactionId: string, ... }
Response: HTML with postMessage script

The webhook should:

  1. Validate the authentication
  2. Process the payment
  3. Return HTML that sends a postMessage to the parent window:
<script>
  const message = {
    type: '3DS_COMPLETE',
    transactionId: 'xxx',
    success: true,
    payment: { id: 'pay_123', status: 'AUTHORIZED', amount: 100.00, currency: 'USD' }
  }
  
  if (window.parent !== window) window.parent.postMessage(message, '*')
  if (window.opener) window.opener.postMessage(message, '*')
  if (window.top && window.top !== window) window.top.postMessage(message, '*')
</script>

🎨 Styling the Challenge Modal

You can customize the modal appearance:

client.setModalStyles({
  overlay: {
    backgroundColor: 'rgba(0, 0, 0, 0.75)'
  },
  modal: {
    borderRadius: '16px',
    boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)'
  },
  header: {
    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
    padding: '24px'
  }
})

🐛 Troubleshooting

Microform fields not showing

  • Ensure the container element exists in the DOM before calling setupFlexMicroform
  • Check browser console for script loading errors
  • Verify the capture context is valid and not expired

Challenge modal not closing

  • Ensure your webhook returns proper HTML with postMessage script
  • Check that the transactionId in the message matches
  • Verify the message event listener is set up before showing the modal

"Flex SDK not available" error

  • The SDK automatically loads the Flex script, but network issues can cause failures
  • Check your Content Security Policy allows loading from flex.cybersource.com
  • Try manually including the script tag in your HTML

Contributing

See CONTRIBUTING.md for details.

License

MIT License.