Introduction

This document contains instructions for integrating PC SDK into a web application. To better understand how to work with PC, we also recommend reviewing the following documents:

PC SDK for Web allows you to implement the full PC functionality directly in your web application.

PC SDK can store user keys in two types of storage:

  1. Rutoken ECP 3.0 (hereinafter referred to as token). Can only be used for the GOST algorithms.
  2. Browser IndexedDB. Can only be used for the ECDSA algorithm.

Rutoken Plugin is used to work with Rutoken ECP 3.0.
PC WEB SDK independently checks for its presence during initialization (when necessary).

Project integration

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

Add the following line to the .npmrc file in the project root

@safetech:registry=https://nexus.paycontrol.org/repository/npm-external/

Then install pcsdk.

npm i @safetech/pcsdk

After installing the PCSDK library, it can be imported into the project:

import PCSDK from '@safetech/pcsdk';

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

SDK initialization

To initialize the PC SDK library, call the asynchronous method

PCSDK.init(callback?, errorCallback?, options?)

The method accepts 3 optional parameters:

  1. callback(status) - called when the state of connected tokens changes (only if there are newly connected or disconnected tokens).
    The function receives an object:
    { connected: Array<number>, disconnected: Array<number> }
    • connected — array of identifiers of connected tokens
    • disconnected — array of identifiers of tokens that were disconnected
  2. errorCallback(error) - called if an error occurred during periodic polling of token state (for example, if the plugin for working with tokens stopped responding).
  3. options — object with optional configuration parameters:
    { rutokenOnly?: boolean, dev?: boolean, unixTime?: number }
    • The rutokenOnly parameter disables working with IndexedDB (default true)
    • The dev parameter — enables SDK debug mode. When set to true, diagnostic messages about SDK operation are output to the console. Logging is disabled by default.
    • The unixTime parameter defines the system time as a Unix timestamp (in seconds):
      • If the parameter is not provided — time is automatically synchronized via use.ntpjs.org.
      • If set to 0 — the local machine time is used.
      • If a number greater than 0 is provided — the specified time is used.

If the rutokenOnly === true parameter (default value), then on initialization error of Rutoken Plugin, the PCSDK.init() method throws an exception (throw PCError).

In this mode, the library expects that Rutoken Plugin must be available and correctly loaded, so any loading error leads to immediate interruption of initialization.

If rutokenOnly === false, then a plugin loading error does not throw an exception — it will be returned in the Promise result as:

plugin: { ok: false, error: PCError }

The PCSDK.init(callback, errorCallback, options) method — returns a Promise, the result of which is an object:

{
  db: { ok: boolean, error: PCError | null },
  plugin: { ok: boolean, error: PCError | null }
}
import PCSDK from '@safetech/pcsdk';

try {
  let options = {rutokenOnly: false, dev: false};

  const result = await PCSDK.init(callback, errorCallback, options);

  if (!result.db.ok) {
    // Handle user storage initialization error
  }

  if (!result.plugin.ok) {
    // Handle Rutoken plugin initialization error
  }
} catch (error) {
  console.error('PCSDK initialization error:', error);
}

Structure of the error object

The plugin.error object is always present in the response and reflects the plugin state.

  • If the plugin is initialized correctly - the plugin.error value will be null.
  • If an error occurred during initialization - plugin.error returns an object of type PCError containing information about the cause of 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"
    }
  }
}

User registration

The user registration process includes the following stages:

  1. Obtaining personalization data as JSON (including from QR or deeplink) and, optionally, an activation code. See the Architecture and operation principles document for details
  2. Registering the PCUser on the PC server. During this process, PC SDK generates the necessary key sets
  3. Saving the user's keys in storage (token or IndexedDB) for further use

Step 1. Importing PCUser

Determining where to store keys

If you plan to use GOST algorithms, this is only possible with tokens.
In this case, first of all, you need to obtain the list of connected tokens and information about them using the getTokensInfo() method, as well as select the deviceId of the token on which the keys will be stored.

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

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

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

  // Take the first connected token
  const {deviceId} = devices[0];
} catch (error) {
  console.error('Error obtaining token list:', error);
}

If you plan to work only with ECDSA and IndexedDB, then deviceId is not needed

const deviceId = null;

Import

Next, you need to create a PCUser object from a JSON string, specifying deviceId.

Method

PCUsersManager.importUser(source: string | PCUserJSON, deviceId: number | null)

Returns a Promise, the result of which is a PCUser object.

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

// Determining deviceId

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

Step 2. Checking whether PCUser activation is required

Personalization data may be transmitted with a mandatory activation code (see Architecture and operation principles).

Therefore, after importing the PCUser object, you need to:

  1. Check whether activation is required user.isActivated();
  2. If necessary, perform activation using the activation code.

The following method is used for activation

PCUsersManager.activate(user: PCUser, activationCode: string)

The method performs activation code verification for the PCUser object

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

// Importing PCUser

try {
  if (!user.isActivated()) {
    // Activation code is required to activate PCUser
    const activationCode = getActivationCode(); // activation code obtained from user or by other means
    await PCUsersManager.activate(user, activationCode);
    console.log('User successfully activated');
  } else {
    console.log('Activation not required');
  }

  // If there are no errors — the user is successfully imported and 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 object on the PC server.

The following method is used for registration

PCUsersManager.register(user: PCUser, password: string)

The method performs generation of the necessary keys and their registration on the server.

The password parameter can have two meanings, depending on whether a token or IndexedDB is used:

  • when working with a token - this is the token PIN code
  • when working with IndexedDB - this is the password for accessing keys, which will need to be presented when forming confirmations
import {PCUsersManager} from '@safetech/pcsdk';

try {
  // request token PIN code or password for saving in IndexedDB
  const password = promptPassword();

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

// If there are no errors — the user is successfully registered

Step 4. Saving the PCUser object

At the final stage, you need to save the PCUser object.

Each user is saved under a unique name (keyName), which is set by your application (or requested from the User).

If you save the PCUser object in IndexedDB and the device supports WebAuthn (for example, Touch ID, Face ID, or Windows Hello), then biometric authentication can be used instead of a password.

To enable biometrics, you need to perform three checks of flags and the PCUser object type (see PC Server API Reference -> Create User):

  • user.type === 0 - check that the key type is non-GOST
  • user.hasOnlineCredentials() === true - check that the user has the keyFlag activated that allows the use of online credentials
  • user.isWebAuthnAllowed() === true - check that the user is allowed to use WebAuthn

Method

PCUsersManager.store(user: PCUser, keyName: string, password: string, useBiometry: boolean)

saves the user object in token or IndexedDB storage, including activation of biometry-mode when useBiometry === true

WebAuthn check example

function checkBiometry() {
    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 data unavailable');
    }

    } catch (error) {
        console.warn('An error occurred while using biometrics', e);
        useBiometry = false;
    }

    return useBiometry
}

Key saving process example

// request unique key name from client or generate in application
const keyName = promptKeyName();

// request token PIN code or password for saving in IndexedDB
const password = promptPassword();

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

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

// If there are no errors — the user is successfully saved

General key registration scheme

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

// 1. Importing PCUser
let user = null;

try {
  // are we using GOST and tokens?
  const useGOST = false;
  let deviceId = null;

  if (useGOST) {
    const devices = await PCSDK.getTokensInfo();

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

    deviceId = devices[0].deviceId;
  }

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

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

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

// 4. Saving the key
const keyName = promptKeyName();
const useBiometry = await checkBiometry();

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

User removal

Method

PCUsersManager.delete(user: PCUser, password: string | null)

Used to remove a user from the SDK storage.

Depending on the user type, different password behavior is required:

  • ECDSA (user.type === 0) - password is not required for deletion, the password parameter can be passed as null
  • GOST (user.type === 1) - password is required, as deletion is performed on the token

Usage example

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

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

  if (!users.length) {
    console.log('No users found');
    return;
  }

  const user = users[0];

  const password = user.type === 1 ? prompt('Enter Rutoken password') : null;

  await PCUsersManager.delete(user, password);

  console.log('User successfully deleted');
} catch (error) {
  console.error('Error deleting user:', error);
}

Obtaining user certificate information

If your PC system is used together with CA (PKI) tools, the user may have been issued a certificate (see PC Server API Reference -> PKI endpoint)

The following method is used to obtain certificate information

PCUsersManager.getCertificateInfo(user: PCUser)

The method returns a Promise with the following structure:

{
  certRequest?: string,
  certificate?: string,
  status: CertificateStatus
}

Certificate statuses

  • UNDEFINED - request and certificate are absent
  • CERT_REQUEST_INFO_CREATED - request information created, no certificate data
  • CERT_REQUEST_CREATED - certificate request created (certRequest is present), no certificate
  • CERT_ISSUED - certificate issued (certRequest and certificate are present)
  • CERT_ISSUE_ERROR - certificate issuance error
  • CERT_REVOKED - certificate revoked
  • CERT_EXPIRED - certificate expired

Usage example

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

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

  if (!users.length) {
    console.log('No users found');
    return;
  }

  const user = users[0];

  const result = await PCUsersManager.getCertificateInfo(user);

  console.log('Certificate status:', result.status);

  if (result.certRequest) {
    console.log('Certificate request:', result.certRequest);
  }

  if (result.certificate) {
    console.log('Certificate:', result.certificate);
  }
} catch (error) {
  console.error('Error obtaining certificate information:', error);
}

Transaction processing

The general transaction processing scenario 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 loaded
  3. For each user, call PCTransactionsManager.getTransactionList(user: PCUser) and check for transactions
  4. For each transaction, call PCTransactionsManager.getTransaction(user: PCUser, transactionId: string) to obtain data
    • If attachments are present, use PCTransactionsManager.getTransactionBinaryData(user: PCUser, transaction: PCTransaction)
  5. Display the loaded transactions to the user with confirm and decline buttons
  6. Process user actions:
    • request password for key access
    • to confirm a transaction, call the method PCTransactionsManager.sign(user: PCUser, transaction: PCTransaction, password: string)
    • to decline a transaction, call the method PCTransactionsManager.decline(user: PCUser, transaction: PCTransaction, password: string)

Obtaining transaction data

Example of loading available transactions:

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

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

  // filter the list of users
  for (let user of users) {
    if (user.userId === TARGET_USER_ID || user.keyName === TARGET_KEY_NAME) {
      // load the list of transactions
      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 be displayed immediately
          }
        }
      }
    }
  }
} catch (error) {
  console.error('An error occurred while obtaining the transaction', error);
}

The PCTransactionsManager.getTransactionList() and PCTransactionsManager.getTransaction() methods are lightweight and create a small amount of network traffic (a few kilobytes).

The PCTransactionsManager.getTransactionBinaryData() method may use significantly more traffic when loading large attachments.

Displaying transaction data

The transaction must be shown to the user before confirmation or declination. The following methods can be used for convenient display:

  • PCTransaction.getTransactionText() - returns the transaction text (may be null if there is only an attachment).
  • PCTransaction.getSnippet() - returns a brief description (may be null).
  • PCTransaction.getStoredBinaryData() - returns Uint8Array with binary attachment (for example, PDF).
  • PCTransaction.getTextRenderType() - returns the text type: null or 'raw' for plain text, 'markdown' for markdown format.
  • PCTransaction.getSnippetRenderType() - same as above, but for the brief description.

Confirming or declining a transaction

After loading and displaying transaction data, it can be confirmed with the sign() method or declined with the decline() method of the PCTransactionsManager class.

Confirmation process example

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

const password = promptPassword();

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

If a transaction contains an attachment but it was not 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 general operation processing scenario includes the following steps:

  1. Call PCUsersManager.listStorage() to get the list of users from storage
  2. Filter users for whom operations need to be loaded
  3. For PCUser, call PCOperationsManager.getOperationsList(user: PCUser)
  4. If operations are available, obtain each of them using PCOperationsManager.getOperation(user: PCUser, operationId: string).
    The PCOperation object contains data about all transactions included in it
  5. For each transaction in PCOperation.getTransactions(), check whether binary data loading is required by calling PCTransaction.hasBinaryData(), and if necessary, load it via PCTransactionsManager.getTransactionBinaryData(user: PCUser, transaction: PCTransaction)
  6. Display the operation as desired. For more details on transaction display methods, see the "Displaying transaction data" section. In addition to transaction data, the operation description (PCOperation.getDescription()) should also be displayed
    Operations support partial processing: the User can confirm some transactions immediately and postpone others. Therefore, the next step will be to provide an interface that allows the User to choose which transactions they want to process now
  7. Call PCTransaction.processOperation(user: PCUser, operation: PCOperation, password: string, transactionsToConfirm: Array<PCTransaction>, transactionsToDecline: Array<PCTransaction>) to process the operation

If some transactions were not processed, the User can return to them later

Obtaining operation data

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

try {
  let targetUser = null;
  // 1. Get the list of users
  const users = await PCUsersManager.listStorage();

  // 2. Filter the list
  for (let user of users) {
    if (user.userId === TARGET_USER_ID || user.keyName === TARGET_KEY_NAME) {
      targetUser = user;
    }
  }
  if (null == targetUser) {
    return;
  }

  // 3. Get the user's list of operations
  const operationIds = await PCOperationsManager.getOperationsList(targetUser);

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

  // 4. 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();

  // 5. Load binary data of transactions (if any)
  for (const transaction of transactions) {
    if (transaction.hasBinaryData()) {
      try {
        // Load transaction binary data
        await PCTransactionsManager.getTransactionBinaryData(targetUser, transaction);

        // Transaction is now fully loaded and ready for processing
      } catch (error) {
        console.error('Error loading transaction binary data:', err);
      }
    } else {
      // Transaction has no binary data — can be displayed/processed immediately
    }
  }
} catch (error) {
  console.error('An error occurred while loading operations:', error);
}

Processing an operation

To process an operation, you need to specify the list of transactions to confirm and the list of transactions to decline.

Each of the lists can be empty, but not both at the same time. Also, the lists must not overlap.

The example below demonstrates how a PCOperation can be processed.

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

// pcOperation created, transactions loaded together with binary data

/*
 * 6. User selects transactions for processing
 * Each transaction is an object from PCOperation.getTransactions()
 */
const transactionsToConfirm = getTransactionsSelectedForConfirmation();
const transactionsToDecline = getTransactionsSelectedForDeclination();

// Request token PIN code or password for IndexedDB
const password = promptPassword();

// 7. Processing 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 processed successfully.');
    } else {
      console.warn('Some transactions were not processed.');
    }
  } catch (error) {
    console.error('An error occurred while processing the operation:', error);
  }
}

Method

PCOperationsManager.processOperation(
    user: PCUser,
    operation: PCOperation,
    password: string,
    transactionsToConfirm: Array<PCTransaction>,
    transactionsToDecline: Array<PCTransaction>)

processes the passed transactions and returns an object with two fields:

  • confirmationResults - results of processing transactions sent for confirmation
  • declinationResults - results of processing transactions sent for declination

Each element in these arrays contains information about a specific transaction and the processing outcome. The result format looks like this:

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

Where errorCode === 0 means successful processing, and any other values indicate an error.

After processing transactions, the PCOperation object is not updated automatically.

To get the current state of the operation, you need to request it again from the server using PCOperationsManager.getOperation(targetUser, operationId).

Changelog

Version: 2.1.0

New features

  • In the PCSDK.init method, a unixTime parameter has been added to options to control the system time source (Unix timestamp in seconds).

Version: 2.0.1

New features

  • Migration from Webpack to Vite
  • Event collection mechanism when the collect_events flag is present
  • Method for obtaining attachment name for transactions getDataFilename()
  • Ability to sign CMS transactions
  • Ability to sign CSR transactions
  • Method for obtaining certificate and certificate request information PCUsersManager.getCertificateInfo(user: PCUser)
  • Key deletion without password entry for ECDSA
  • User update functionality
  • Method PCUsersManager.importUser(source, deviceId: number | null) — for ECDSA, deviceId is expected to be null

Fixes

  • Rutoken Lite is no longer displayed in the token list
  • Loading large attachments in transactions no longer times out
  • Various fixes and improvements