Getting Started

This document provides instructions for embedding PC WEB SDK in web application. In order to understand better how to work with PC we recommend you to read the following documents as well:

  • Architecture and functionality presentation
  • Architecture and functionality

PC WEB SDK allows you to implement the full functionality of PC directly in your web application.

Project integration

The PC WEB SDK library can be imported into your project from the SafeTech npm repository.

You need to add this string: @safetech:registry=https://nexus.paycontrol.org/repository/npm/ to the .npmrc file in root of the projecr, then install pcsdk

npm i @safetech/pcsdk

After installing the library, PCSDK can be imported into the project.

import PCSDK from @safetech/pcsdk

Your web application must use a single instance of the PCSDK class during operation.

SDK Initialization

To initialize PC SDK, call the asynchronous method PCSDK.init(callback?, errorCallback?, options?).

The method accepts 3 optional parameters: 1. callback(status) — invoked each time the state of connected tokens changes. The function receives the following object: { connected: Array<number>, disconnected: Array<number> } * connected — an array of IDs of connected tokens * disconnected — an array of IDs of tokens that were disconnected 2. errorCallback(error) — invoked if an error occurs during the periodic polling of token connection status (for example, if the plugin stops responding). 3. options — an optional configuration object: { rutokenOnly?: boolean }. The rutokenOnly parameter defines which types of tokens the library is allowed to use: * true (default) — the library operates exclusively with Rutoken. * false — allows the use of ECDSA tokens and other supported token types in addition to Rutoken.

If rutokenOnly = true (default), and a Rutoken plugin initialization error occurs, the PCSDK.init() method throws an exception (throw Error). In this mode, the library expects the Rutoken plugin to be available and fully operational; therefore, any plugin loading error results in immediate termination of the initialization process.

If rutokenOnly = false, plugin initialization errors do not result in an exception. Instead, they are returned in the Promise result as: plugin: { ok: false, error: PCError }.

The PCSDK.init(callback, errorCallback) method returns a Promise that resolves to the following object:

{
  db: { ok: boolean, error: PCError | null },
  plugin: { ok: boolean, error: PCError | null }
}

PCSDK must be initialized before calling any other methods of the library.

import PCSDK from @safetech/pcsdk
try {
  const result = await PCSDK.init(callback, errorCallback);

  if (!result.db.ok) {
    // The user storage initialization error needs to be handled here
  }

  if (!result.plugin.ok) {
    // The Rutoken plugin initialization error needs to be handled here
  }
} catch (error) {
  console.error('PCSDK initialization error:', error);
}

Structure of the error object

The plugin.error field is always present in the response and reflects the current plugin state. * If the plugin initializes successfully, plugin.error is null. * If an initialization error occurs, plugin.error contains a PCError object with details about the failure.

Example of successful initialization:

{
  db: { ok: true, error: null },
  plugin: { ok: true, error: null }
}

Example with plugin error:

{
  db: { ok: true, error: null },
  plugin: {
    ok: false,
    error: {
      code: 3,
      errorName: "PC_ERROR_RUTOKEN_EXTENSION_NOT_INSTALLED",
      link: "https://chromewebstore.google.com/detail/ohedcglhbbfdgaogjhcclacoccbagkjg",
      message: "Rutoken extension is not installed",
      name: "PCError"
    }
  }
}

Registering users

The process of user registration includes the following steps:

  1. Getting the personalization data from the PC Server (via deeplink, deeplink + activation code) see Architecture and functionality document for more details on this.
  2. Registering a PC User on PC Server. PCSDK generates the required key sets during this process.
  3. Storing user's keys in token storage for further usage

Step 1. Import PCUser from the appropriate source

First of all, you need to get a list of connected tokens and information about them using the method PCSDK.getTokensInfo().

Then, construct a PCUser object (also referred to as a key) from a string containing JSON data — either extracted from a deeplink or received via the PC Server API — along with the deviceId where the user will be stored.

The PCUsersManager.importUser(source: string | PCUserJSON, deviceId: number) method returns a Promise that resolves to a PCUser object.

import PCSDK, { PCUsersManager } from @safetech/pcsdk’;

try {
  /* 
    devices returns:
    Array<{ 
    label: string,
    serial: string, 
    leftSpace: number,
    deviceId: number,
    }>
  */
  const devices = await PCSDK.getTokensInfo();

  const user = await PCUsersManager.importUser(source, deviceId);
  console.log('Import completed:', user);
} catch(error) {
  console.error('Error importing user:', error);
}

Step 2. Check if PCUser requires activation

Depending on your infrastructure, personalization data may be delivered via deeplink, deeplink + activation code, or JSON.

Before registration: 1. Import the data into a PCUser object 2. Check if activation is required: (user.isActivated() || user.hasKeyPair()) 3. If necessary, perform activation using the activation code.

For activation, use the method PCUsersManager.activate(user: PCUser, activationCode: string).

import PCSDK, { PCUsersManager } from @safetech/pcsdk’;

try {
  const devices = await PCSDK.getTokensInfo();

  if (!devices.length) {
    console.log('No connected tokens');
    return;
  }

  const { deviceId } = devices[0];
  const user = await PCUsersManager.importUser(source, deviceId);

  if (!user.isActivated()) {
    // To activate PCUser, you need an activation code
    // obtained in accordance with the rules of your infrastructure
    const activationCode = getActivationCode();
    await PCUsersManager.activate(user, activationCode);
    console.log('User successfully activated');
  } else {
    console.log('Activation not required');
  }

  // If there are no errors, the user has been successfully imported and is ready for registration.
} catch(error) {
  console.error('Error importing or activating user:', error);
}

Step 3. Registering the key on the PC Server

Now you can register the PCUser on the PC Server. A key pair is generated: the public key is sent to the server, while the private key is encrypted with the password and an additional encryption layer. Use the PCUsersManager.register(user: PCUser, password: string) method to perform the registration.

import { PCUsersManager } from '@safetech/pcsdk';

try {
// request the token password
const password = promptPassword();

await PCUsersManager.register(user, password);
} catch (error) {
console.error('An error occurred during registration', error);
}

// If no errors occurred, the user has been successfully registered

PCSDK never stores entered passwords. It is the client’s responsibility to manage and remember them.

Step 4. Store the PCUser object in the token storage

At the final stage, you need to save the PCUser object in the token’s storage. Each user is stored under a unique name (keyName) defined by the client application

The keys used for transaction confirmation (the HMAC key and the private ECDSA signing key) are additionally protected by the token password and encryption.

If the device supports WebAuthn (e.g., Touch ID, Face ID, or Windows Hello), biometric authentication can be used instead of a password. To enable this, set the useBiometry parameter, which specifies whether biometric verification should be activated when storing the key.

In addition to device support, enabling WebAuthn requires validating two user-level flags: * user.hasOnlineCredentials() — Returns true if the user has the keyFlag that enables the use of online credentials. If this flag is disabled, the SDK must operate using only the local password, without requesting salt + online credentials from the server. These values are required by the SDK to derive the cryptographic keys. Although WebAuthn and online credentials are different mechanisms, WebAuthn can only be enabled if online credentials are available for this user. * user.isWebAuthnAllowed() — Returns true if WebAuthn is allowed for this user. For example, this method returns false if biometric authentication was disabled in the user’s profile or by policy.

The PCUsersManager.store(user: PCUser, keyName: string, password: string, useBiometry: boolean) method saves the user object into the token storage, including enabling biometry-mode when useBiometry = true.

Example WebAuthn check:

let useBiometry = false;

try {
  const isPlatformAuthenticatorAvailable =
    typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function'
      ? await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
      : false;

  if (
    user.type === 0 &&
    isPlatformAuthenticatorAvailable &&
    user.isWebAuthnAllowed() &&
    user.hasOnlineCredentials()
  ) {
    useBiometry = true;
  } else {
    console.warn('Biometric authentication is not available');
  }

} catch (error) {
  console.warn('Error checking biometrics:', error);
  useBiometry = false;
}

return useBiometry;

Example of the key storage process:

// request a unique key name from the client or generate one within the app
const keyName = promptKeyName();

// request the token password
const password = promptPassword();

// whether to use biometrics (WebAuthn)
const useBiometry = await askUserToUseBiometry();

// store the key in the token storage
try {
  await PCUsersManager.store(user, keyName, password, useBiometry);
} catch (error) {
  console.error('Error while storing user:', error);
}

// If no errors occurred, the user has been successfully stored

General key registration flow

The overall process of registration may have the following structure:

import PCSDK, { PCUsersManager } from '@safetech/pcsdk';

// 1. Import PCUser
let user = null;

try {
  const devices = await PCSDK.getTokensInfo();

  if (!devices.length) {
    console.log('No connected tokens');
    return;
  }

  const { deviceId } = devices[0];
  user = await PCUsersManager.importUser(source, deviceId);
} catch (error) {
  console.error('Error importing user:', error);
}

// 2. Check if activation is required
try {
  if (!user.isActivated()) {
    const activationCode = getActivationCode();
    await PCUsersManager.activate(user, activationCode);
  } else {
    // activation not required
  }
} catch (error) {
  console.error('Error activating user:', error);
}

// 3. Register the user
try {
  const password = promptPassword();
  await PCUsersManager.register(user, password);
} catch (error) {
  console.error('An error occurred during registration', error);
}

// 4. Store the key
const keyName = promptKeyName();
const password = promptPassword();
const useBiometry = await askUserToUseBiometry();

try {
  await PCUsersManager.store(user, keyName, password, useBiometry);
} catch (error) {
  console.error('Error storing user:', error);
}

Process transactions

The general transaction processing flow includes the following steps:

  1. Call PCUsersManager.listStorage() to get the list of users from storage.
  2. Filter users for whom transactions need to be retrieved.
  3. For each user, call PCTransactionsManager.getTransactionList(user: PCUser) to check for available transactions.
  4. For each transaction, call PCTransactionsManager.getTransaction(user: PCUser, transactionId: string) to retrieve its data.
    • If the transaction has attachments, use PCTransactionsManager.getTransactionBinaryData(user: PCUser, transaction: PCTransaction).
  5. Display the retrieved transactions to the user with "Confirm" and "Decline" buttons.
  6. Handle user actions:
    • request the password.
    • to confirm a transaction, call PCTransactionsManager.sign(user: PCUser, transaction: PCTransaction, password: string).
    • to decline a transaction, call PCTransactionsManager.decline(user: PCUser, transaction: PCTransaction, password: string).

Getting transaction data

This code snippet demonstrates how to download available transactions and get ready to display them.

import { PCUsersManager, PCTransactionsManager } from '@safetech/pcsdk';

try {
  const users = await PCUsersManager.listStorage();

  // filtering the list of users
  for (let user of users) {
    if (user.userId === TARGET_USER_ID || user.keyName === TARGET_KEY_NAME) {
      // load transaction list
      const transactionsIdArr = await PCTransactionsManager.getTransactionList(user);

      if (transactionsIdArr.length) {
        for (let transactionId of transactionsIdArr) {
          const transaction = await PCTransactionsManager.getTransaction(user, transactionId);

          if (transaction.hasBinaryData()) {
            await PCTransactionsManager.getTransactionBinaryData(user, transaction);
          } else {
            // no attachments — can display immediately
          }
        }
      }
    }
  }
} catch (error) {
  console.error('An error occurred while retrieving transactions', error);
}

The methods PCTransactionsManager.getTransactionList() and PCTransactionsManager.getTransaction() are lightweight and generate minimal network traffic (a few kilobytes).
The method PCTransactionsManager.getTransactionBinaryData() may use significantly more bandwidth when downloading large attachments.

Displaying transaction Data

A transaction must be displayed to the user before confirmation or rejection.
For convenient display, you can use the following methods:

  • PCTransaction.getTransactionText() — returns the transaction text. Might be null if the transaction contains only an attachment.
  • PCTransaction.getSnippet() — returns a short description of the transaction, useful for displaying it in a list. Might be null.
  • PCTransaction.getStoredBinaryData() — returns a Uint8Array pointing to the binary attachment (for example, a PDF document). Might be null if the transaction has no attachment.
  • PCTransaction.getTextRenderType() — returns the render type of the transaction text. Use this method when PCTransaction.text has a non-null and non-empty value to render the text correctly. Returns either null or 'raw' for plain text, and 'markdown' if the transaction text uses Markdown syntax.
  • PCTransaction.getSnippetRenderType() — similar to getTextRenderType(), but applies to the transaction snippet.

Confirm or decline the transaction

As soon as you have downloaded and displayed transaction data, it can be confirmed through sign method or declined with decline method of PCTransactionsManager class. The sample confirmation flow looks like the following:

import { PCTransactionsManager } from '@safetech/pcsdk';

const password = promptPassword();

if (confirmationButtonPressed) {
  PCTransactionsManager.sign(user, transaction, password)
    .then(/* notify user of successful signing */)
    .catch((error) => {/* handle error */});
} else {
  PCTransactionsManager.decline(user, transaction, password)
    .then(/* notify user of successful rejection */)
    .catch((error) => {/* handle error */});
}

If a transaction contains an attachment but it has not been loaded via PCTransactionsManager.getTransactionBinaryData(), the transaction will not be processed.

Working with Operations

An operation (PCOperation) is a set of related transactions (PCTransaction) that can be processed in a single request. This reduces the number of network calls and speeds up execution.

The workflow for processing operations may include the following steps:

  1. Call PCUsersManager.listStorage() and, if necessary, filter the retrieved users.
  2. For each PCUser, call PCOperationsManager.getOperationsList(user: PCUser) to check if there are any operations pending processing.
  3. If operations are available, fetch each one using PCOperationsManager.getOperation(user: PCUser, operationId: string). The PCOperation object contains data for all transactions it includes. However, the binary data for each transaction must be loaded separately.
  4. For each transaction returned by PCOperation.getTransactions(), check if binary data is required by calling PCTransaction.hasBinaryData(), and if necessary, load it via PCTransactionsManager.getTransactionBinaryData(user: PCUser, transaction: PCTransaction).
  5. Display the operation as needed. For more details on rendering transactions, see the «Displaying transaction Data» section. In addition to transaction data, the operation description (PCOperation.getDescription()) should also be displayed.
  6. Operations support partial processing: the client can confirm some transactions immediately and defer others. The next step is to provide an interface allowing the client to choose which transactions to process now, using PCTransactionsManager.processOperation(user: PCUser, operation: PCOperation, password: string, transactionsToConfirm: Array<PCTransaction>, transactionsToDecline: Array<PCTransaction>).
  7. If some transactions are not processed, the client can return to them later.

Retrieving Operation Data

The following code snippet demonstrates how to properly fetch PCOperation data from the PC server so that it can be correctly displayed and processed.

import { PCUsersManager, PCOperationsManager } from '@safetech/pcsdk';

// 1. Locate the user (PCUser) according to your application's logic
const targetUser = PCUsersManager.getById(TARGET_USER_ID);

// 2. Get the user's operation list
try {
  const operationIds = await PCOperationsManager.getOperationsList(targetUser);

  if (!operationIds || operationIds.length === 0) {
    // List is empty
    return;
  }

  // 3. Load operation data
  const operationId = operationIds[0];
  const pcOperation = await PCOperationsManager.getOperation(targetUser, operationId);

  // Operation description
  const description = pcOperation.getDescription();

  // List of transactions
  const transactions = pcOperation.getTransactions();

  // 4. Load transaction binary data (if any)
  for (const transaction of transactions) {
    if (transaction.hasBinaryData()) {
      try {
        await PCTransactionsManager.getTransactionBinaryData(targetUser, transaction);
        // Transaction is now fully loaded and ready for processing
      } catch (err) {
        console.error("Error loading transaction binary data:", err);
      }
    } else {
      // No binary data — transaction can be displayed/processed immediately
    }
  }
} catch (error) {
  console.error("An error occurred while loading operations:", error);
}

To process an operation, you must provide a list of transactions to confirm and a list to decline. Each list may be empty, but not both simultaneously. The lists must not overlap.

Processing an Operation

The example below demonstrates how a PCOperation can be processed. It assumes that binary data (attachments) has already been loaded for all transactions that will be processed.

import { PCUsersManager, PCOperationsManager } from '@safetech/pcsdk';

const pcUser = PCUsersManager.getById(TARGET_USER_ID);
const operationId = TARGET_OPERATION_ID;

const pcOperation = await PCOperationsManager.getOperation(pcUser, operationId);

/*
 * 1. The client must provide lists of transactions to confirm or decline.
 * Each element must be an object from PCOperation.getTransactions().
 */
const transactionsToConfirm = getTransactionsSelectedForConfirmation();
const transactionsToDecline = getTransactionsSelectedForDeclination();

// 2. Request password
const password = promptPassword();

// 3. Process the operation
async function processOperation() {
  try {
    const { confirmationResults, declinationResults } =
      await PCOperationsManager.processOperation(
        pcUser,
        pcOperation,
        password,
        transactionsToConfirm,
        transactionsToDecline
      );

    const allResults = [...confirmationResults, ...declinationResults];

    const hasErrors = allResults.some(r => r.result.errorCode !== 0);

    if (!hasErrors) {
      console.log("All transactions were processed successfully.");
    } else {
      console.warn("Some transactions were not processed.");
    }
  } catch (error) {
    console.error("An error occurred while processing the operation:", error);
  }
}

The function PCOperationsManager.processOperation(user: PCUser, operation: PCOperation, password: string, transactionsToConfirm: Array<PCTransaction>, transactionsToDecline: Array<PCTransaction>) processes the provided transactions and returns an object with two fields: * confirmationResults — results of transactions sent for confirmation * declinationResults — results of transactions sent for declination

Each element in these arrays contains information about a specific transaction and the outcome of its processing. The result format is:

const confirmationResults = {
  transactionId: "string",
  result: {
    errorCode: 0,
    errorMessage: "Success"
  }
}

Where errorCode = 0 indicates successful processing, and any other value indicates an error.

After processing, the PCOperation object is not automatically updated. To get the current state of the operation, you need to fetch it again from the server using: PCOperationsManager.getOperation(targetUser, operationId)