Skip to main content

How the Demo app uses InCountry

Table of сontents

InCountry Demo Application is a lightweight application that demonstrates the way how data residency capabilities of the InCountry platform are implemented into a generic web application. The primary focus is on the following common tasks:

  1. redaction and unredaction of sensitive data handled by the demo application;

  2. search of sensitive data stored on the InCountry platform through REST API

  3. executing resident functions stored on the InCountry platform through REST API

Functionality

The Demo Application works with three countries:

  1. United States of America (replicated model)

  2. Saudi Arabia (restricted model)

  3. China (redacted model)

Depending on the used data regulation model, the application proxies data requests through Border considering the country-specific behavior:

  1. United States: clear-text values are saved to the InCountry platform and then clear-text values are passed to the application backend for saving.

  2. Saudi Arabia and China: clear-text values are saved to the InCountry platform and then redaction strategies are applied. Border redacts sensitive values and outputs unique tokens that are further proxied to the application backend which saves them to the application database.

The Demo Application handles all CRUD operations for customer records through Border. Each record has the following fields:

FieldRedaction strategySearchable
Full NamealphaNumericPersistent
key1
EmailemailPersistent
key2
PhonealphaNumericPersistent
key3
Vehiclenon-regulated
Maintenance expensesnumeric
Anticipated expensesregulated
calculated by resident function
Countrynon-regulated

Customer records stored on the InCountry platform are queried by the application and displayed in the table with pagination. The visibility of sensitive values depends on the user who views this data and internal logic which queries sensitive data either from the application database or from the InCountry platform.

In the top part of the Demo Application, you can select a user role on whose behalf you want to view customer records. Depending on the selected user role, the table will be refreshed and either redacted or clear-text values will be displayed to you. This illustrates how different data regulation models work depending on the country which the agent accesses the application from. Inline hints are displayed in each row for the user explaining what model is used to handle records and why values are redacted or not.

Based on the selected country, the application performs a GET request to the /users_list endpoint routed through the corresponding Border instance and the list of records (with clear-text values) is returned. If the data regulation policy forbids cross-border transfers (redacted) and the agent comes from the country different from the country of origin, the application queries data from the application database directly (redacted values are returned).

The Demo Application supports the record lookup, as follows:

  • partial text-match search for the Full name and Email fields (3 characters in a search query at least)

  • full-text match for the Phone field

The record lookup is executed from the frontend in the redacted and restricted models using the application and performed through REST API. In the replicated model, the record lookup is performed against the application’s backend and its database (not against the InCountry platform).

The application supports creation of new customer records. The application opens the dialog form, as follows:

Instead of manual input of values for new customer records, the application can generate customer records with fake data when you click the Generate a random record button. After the form submission, a new record is created with the POST request to the /users endpoint (passed through the corresponding Border instance based on the record-to-country attribution) and then displayed on the list with other records. Along with a request to create a record, requests are also made to validate the email (if such an email already exists, then the record cannot be created).

By clicking a specific row within the table, the application opens a page with details about a specific customer record (the GET request to the /users/:id endpoint):

The application allows the deletion of customer records (by executing a POST request to the /delete_users/:id endpoint). In this case, the record is deleted both from the application database and from the InCountry platform.

The application allows the editing of records in the country of origin where they are stored. When clicking the Edit customer button, the modal dialog for editing customer records appears:

The edit operation is executed through the POST request to the /update_users/:id endpoint.

The application supports the remote calculation of the Anticipated expenses value, which is implemented with the resident function called out on the InCountry platform in the record’s country of origin.

When clicking the Click to count forecast button, the following flow of actions is executed:

  1. a request to the application’s backend is performed to get an OAuth2 token for the InCountry platform (if such an active token is not already stored already in browser). OAuth2 credentials for the requested country are hardcoded in the application’s backend:

    curl 'https://se-demo-border-mt-01.qa.incountry.io/get_oauth_token' \
    -H 'accept: application/json, text/plain, */*' \
    ...
    --data-raw '{"country":"sa"}' \
    --compressed
  2. a request to REST API is executed to call a pre-defined resident function (demo-forecast).

If a role is selected in which it is impossible to view clear-text customer data, or an Application Database is selected as a data source, then it is impossible to get a forecast according to compliance:

OAuth2 credentials to run a resident function are hardcoded in the application’s backend for the corresponding environment where this function was created. OAuth2 credentials (environment ID, client ID and client secret) are configured as environment variables in the application backend.

note

Records are automatically deleted every day at midnight by the time of the country where the demo application’s backend server is deployed. This is done to reset the default dataset with customer records. The default dataset includes 12 customer records that are not deleted automatically, but only reset to their original values.

Data regulation models

The demo application implements three separate data flows, one per each data regulation model, as follows:

  1. China (redacted)

  2. Saudi Arabia (restricted)

  3. United States (replicated)

Specifics of each data regulation model can be found at https://docs.incountry.com/other/data-models/.

The redacted model applies to records coming from China, restricted to records from Saudi Arabia, and replicated for records from United States.

In the replication model, you don’t need to tokenize sensitive data just save the primary copy in the country of origin, and then you can replicate it elsewhere. To enable this behavior on Border, you need to add the "mode": "replication" flag into to all corresponding redaction rules for the create and update operations in the Border configuration. For example, in the redaction rule for the POST request to the /users endpoint redaction rule:

{
"collectionName": "users",
"entityErrorCorrectionFieldPath": "$.email",
"entityIdPath": "$.id",
"globalEntityId": true,
"method": "POST",
"mode": "replication", // NEW FIELD FOR REPLICATION MODE
"path": "/users",
"searchable": {
"key1": "$.fullName",
"key2": "$.email",
"key3": "$.phone"
},
"strategies": [
{
"path": "$.fullName",
"strategy": "alphaNumericPersistent"
},
{
"path": "$.email",
"strategy": "emailPersistent"
},
{
"path": "$.phone",
"strategy": "email"
},
{
"path": "$.yearlySpending",
"strategy": "numeric"
}
]
}

Since data is not redacted in the replication model, it is displayed as clear-text values in the application even if the agent is not from the United States. Moreover, since data is not redacted on Border, it passes through Border without changes and is saved in the application database as clear-text values too.

In the restricted model sensitive data is redacted, but when you query such records for display in the application, Border unredacts this data and the demo application shows clear-text values. Here you should consider that you can perform all the essential operations on sensitive data but the record must remain in the country of origin on the InCountry platform.

In the redacted model, sensitive data is always redacted. To query it as clear-text values, you need to select the user from the same country which the record is attributed to.

Integration Developer Guide

To implement the processing of CRUD operations with Border, you need to make a Border configuration for each endpoint handling sensitive data on the InCountry Portal as described in our documentation. Once the Border configuration is complete, you need to route CRUD requests through the allocated Border URL.

To implement resident functions, you need to use the REST API as described at https://docs.incountry.com/rest-api/.

note

When you use Border and REST API, you need to use the same environment (environment ID) and the same encryption keys, so regulated records are properly handled (encrypted/decrypted). You need to create a Border and REST API service within the same environment on the InCountry Portal.

If you need to work with multiple countries within your application, you need to use several Border and REST API instances, each one working with a specific country. Within the application, you need to route requests to the appropriate Border and REST API instances based on the country selected by the user on the application frontend.

Record create endpoint

To integrate the record create endpoint, you need to specify this endpoint in the Border configuration. For example, in the Demo Application, the POST /users endpoint is used.

The request payload to the POST /users endpoint looks as follows:

Request payload

{
"fullName": "John",
"email": "john@example.com",
"phone": "+7929",
"vehicle": "Ford",
"yearlySpending": 123,
"country": "us"
}

The response body from the POST /users endpoint looks as follows:

Response body

{
"fullName": "John",
"email": "john@example.com",
"phone": "+7929",
"vehicle": "Ford",
"yearlySpending": 123,
"country": "us",
"id": "rtRGW7xvA"
}

This endpoint adds a new customer record to the application database and the response returns the record’s values.

If you want to redact regulated data, for example, store clear-text values on the InCountry platform and save the redacted (tokenized/hashed data) to the application database, you need to configure a redaction rule. For the details on configuration of redaction rules, please check our documentation.

ParameterDescription
collectionNameDefines an abstract entity (collection) that helps Border to differentiate records pertaining to different objects even when they have the same identifier. Border requires creation of a Border-specific ID which is used if the globalEntityId field is set as false. For example, if there are different redaction rules with objects whose original application IDs match but with different collection names, Border-specific ID will be different (for example, you need to redact a customer record with id 12345 and a lead record record with id 12345, the collectionName value will help Border to differentiate between these records and properly handle them as unique records without collisions).
entityIdPathJSON path which points to the original application ID within the response body.
entityErrorCorrectionFieldPathJSON path which points to the secondary application ID within the response body. It is optional and is used for execution of error correction routines. Usually it is a record unique identification, for example, email address.
globalEntityIdDefines if Border uses the original application ID (if true) or calculates and uses the Border-specific ID (if false) itself. It is better to use globalEntityId: true if the original application ID is a non-human readable automatically incremented value.
methodSpecifies the request type (for example, POST, UPDATE, PUT, and so on), the path fields specifies the endpoint path.
searchableLists fields from the request payload that should be saved to special searchable keys like key1-key25 (string fields) and range_key1-range_key5 (integer fields). Border will save all the regulated fields from the request payload to the InCountry platform within the body field, and additionally will save the fullName field to key1, the email field to key2, and the phone field to key3. It is used record lookup.
strategiesSpecifies which fields are regulated (fields that will be saved to the InCountry platform) and how their sensitive values should be redacted. For example, fullName is a regulated field which will be redacted with the alphaNumericPersistent strategy

The resulting Border configuration for the record create endpoint looks like this:

Border configuration - record create endpoint (redaction rule)

{
"collectionName": "customers",
"entityIdPath": "$.id",
"entityErrorCorrectionFieldPath": "$.email",
"globalEntityId": true,
"method": "POST",
"path": "/users",
"searchable": {
"key1": "$.fullName",
"key2": "$.email",
"key3": "$.phone"
},
"strategies": [
{
"path": "$.fullName",
"strategy": "alphaNumericPersistent"
},
{
"path": "$.email",
"strategy": "emailPersistent",
},
{
"path": "$.phone",
"strategy": "alphaNumericPersistent"
},
{
"path": "$.yearlySpending",
"strategy": "numeric"
}
]
}

Once the redaction rule has been configured, your application is almost ready to work with Border. The only thing you need to do is to modify your application, so the record creation request should be proxied through Border (https://{BORDER_URL}/users) instead of the original application URL (https://{ORIGINAL_APPLICATION_URL}/users). So the request goes first to Border, regulated values are saved to the InCountry platform, and then redacted values (tokens/hashes) are passed to the application backend, which satisfies the compliance regulations.

note

You need to create such configuration for each endpoint you want to operate with and within the application itself you need to properly route records based on their country attribution, for example, the Country field.

If you need to query regulated data from the InCountry platform (clear-text values), for example, when reading records, you need to configure unredaction rules. For the details on configuration of redaction rules, please check our documentation.

In the unredaction rule, you need to list all collections that were used for redaction. In our demo application, we use only the users collection. In this collection, the entityIdPath and entityErrorCorrectionFieldPath fields are JSON paths in the response body where original application ID and secondary application ID are stored.

As mentioned above, the globalEntityId field indicates whether Border will use the original application ID InCountry (if true) or will calculate and use Border-specific ID (if false) itself. It is better to use globalEntityId: true if the original application ID is a non-human readable automatically incremented value.

The name field is a collection name.

In the strategies section, you need to specify which fields are regulated and should be unredacted. If the field is an error correction field (if its field is specified in entityErrorCorrectionFieldPath), it should be marked with the isErrorCorrectionField flag.

The configured unredaction rule should look like this:

Border configuration - record create endpoint (unredaction rule)

{
"collections": [
{
"entityErrorCorrectionFieldPath": "$.email",
"entityIdPath": "$.id",
"globalEntityId": true,
"name": "customers",
"strategies": [
{
"path": "$.fullName"
},
{
"isErrorCorrectionField": true,
"path": "$.email"
},
{
"path": "$.phone"
},
{
"path": "$.yearlySpending"
}
]
}
],
"method": "POST",
"path": "/users"
}

If the unredaction rule works correctly, the response body passed through Border should include clear-text values just as in the original response. Otherwise, values are redacted.

Note that the vehicle field is non-regulated, therefore it is not redacted and not saved to the InCountry platform, so passing through Border it reaches the application database unchanged.

The record create endpoint /users is used in the demo application when adding new customer records. Based on the country attribution (the Country field), a request is routed through one of three Border instances in USA, China, or Saudi Arabia.

tip

To integrate the record creation endpoint, you need to do the following:

  1. Add a redaction rule for the record creation endpoint in the Border configuration. Ensure that you set the correct entityIdPath field.

  2. Add an unredaction rule the record creation endpoint in the Border configuration.

  3. Switch the record creation endpoint URL from the application backend URL to Border URL. Add additional logic to route requests for multiple countries if needed.

Demo application before integration with the InCountry platform:

const APPLICATION_BACKEND_URL = 'https://app-backend-url.com/users';

interface RequestDataType {
fullName: string;
email: string;
phone: string;
vehicle: string;
yearlySpending: number;
country: string;
}

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

public createUser = async (data: RequestDataType) => {
const url = new URL(APPLICATION_BACKEND_URL);
const response = await post<RequestDataType, UserDataType>(url.toString(), data);

return response;
};

Demo application after integration with the InCountry platform:

const CN_BORDER_URL = 'https://cn-proxy-mt-01.alicloud.zhaoerxing.top/x-inc-demoapp';
const SA_BORDER_URL = 'https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp';
const US_BORDER_URL = 'https://us-proxy-mt-01.uat.incountry.io/x-inc-demoapp';

const PROXY_URLS = {
cn: CN_BORDER_URL + '/users',
sa: SA_BORDER_URL + '/users',
us: US_BORDER_URL + '/users',
}

interface RequestDataType {
fullName: string;
email: string;
phone: string;
vehicle: string;
yearlySpending: number;
country: string;
}

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

public createUser = async (data: RequestDataType) => {
const url = new URL(PROXY_URLS[data.country]);
const response = await post<RequestDataType, UserDataType>(url.toString(), data);

return response;
};

Here post is some method async post<T, U>(url: string, data: T) which sends an authenticated POST request to URL with T type request body and responds with U type response body.

Record read endpoints

Several record read endpoints are used in the demo application, for example, GET /users_list and GET /users/:id. The first one is used to get a whole list of customers, and the other is used to fetch details of a specific customer.

While reading data, you don't usually need to redact data within the request payload. So you don't need to create a redaction rule for the record read endpoints. You need an unredaction rule to fetch clear-text values from the InCountry platform.

The response body for the GET /users_list endpoint looks as follows:

Response body

{
"data": [
{
"fullName": "Marley Hegmann",
"email": "Kory88@yahoo.com",
"phone": "+31744560833",
"vehicle": "Jeep Civic",
"yearlySpending": 60080,
"country": "sa",
"id": "rwR66AUzw"
},
{
"fullName": "Jack Columbus",
"email": "columb67@google.com",
"phone": "+26768484047",
"vehicle": "Audi A4",
"yearlySpending": 42300,
"country": "us",
"id": "rtRGW7xvA"
}
],
"draw": 1,
"recordsTotal": 1
}

This endpoint returns all customer records to the demo application for display.

The recordsTotal field is included in the response for pagination purposes.

note

Note that since there are several records data in the response body, JSON paths specified within entityErrorCorrectionFieldPath and entityIdPath are designed so that you can pick up IDs from all records on the list.

The unredaction rule for the record read endpoint looks as follows:

Border configuration - all records read endpoint (unredaction rule)

{
"collections": [
{
"entityErrorCorrectionFieldPath": "$.data[*].email",
"entityIdPath": "$.data[*].id",
"globalEntityId": true,
"name": "users",
"strategies": [
{
"path": "$.fullName"
},
{
"isErrorCorrectionField": true,
"path": "$.email"
},
{
"path": "$.phone"
},
{
"path": "$.yearlySpending"
}
]
}
],
"method": "GET",
"path": "/users_list?(.+)"
}

The response body for the GET /users/:id endpoint:

Response body

{
"fullName": "Marley Hegmann",
"email": "Kory88@yahoo.com",
"phone": "+31744560833",
"vehicle": "Jeep Civic",
"yearlySpending": 60080,
"country": "sa",
"id": "rwR66AUzw"
}

This endpoint returns a specific customer record to the demo application. You can create a similar unredaction rule for this endpoint:

Border configuration - record read endpoint (unredaction rule)

{
"collections": [
{
"entityErrorCorrectionFieldPath": "$.email",
"entityIdPath": "$.id",
"globalEntityId": true,
"name": "users",
"strategies": [
{
"path": "$.fullName"
},
{
"isErrorCorrectionField": true,
"path": "$.email"
},
{
"path": "$.phone"
},
{
"path": "$.yearlySpending"
}
]
}
],
"method": "GET",
"path": "/users/?(.+)"
}

After saving the Border configuration, the application frontend will fetch data through Border (you need to switch the application backend URL to Border URL) and Border will unredact data. If unredaction works correctly, the response passed through Border will include clear-text values just as in the original response. If not, values will be redacted (tokens/hashes).

Based on the user and record’s attribution to a specific country, the request is routed through one of the three Border instances (USA, China, or Saudi Arabia). But if the user comes from Saudi Arabia and tries to open records from China, these records will still be redacted for this user due to the country mismatch and used data regulation model.

tip

To integrate the record read endpoint into your application, you need to do the following:

  1. Add an unredaction rule for the record read endpoints in the Border configuration.

  2. Switch the record read endpoint URL from the application backend URL to Border URL. Add an additional logic to route requests for multiple countries if needed.

Below, get is some method async get<T>(url: string) which sends authenticated GET request to url and responds with T type response body.

1. The part of the application that fetches all customer records should undergo the following modifications:

Demo application before integration with the InCountry platform:

const APPLICATION_BACKEND_URL = 'https://app-backend-url.com/users_list';

interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

interface UsersDataResponse {
data: UserDataType[];
recordsTotal: number;
}

const fetchUsers = async (start: number, length: number) => {
const url = new URL(APPLICATION_BACKEND_URL);
url.searchParams.append('start', start.toString());
url.searchParams.append('length', length.toString());
const response = await this.get<UsersDataResponse>(url.toString());

return response;
};

Demo application after integration with the InCountry platform:

const CN_BORDER_URL = 'https://cn-proxy-mt-01.alicloud.zhaoerxing.top/x-inc-demoapp';
const SA_BORDER_URL = 'https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp';
const US_BORDER_URL = 'https://us-proxy-mt-01.uat.incountry.io/x-inc-demoapp';

const PROXY_URLS = {
cn: CN_BORDER_URL + '/users_list',
sa: SA_BORDER_URL + '/users_list',
us: US_BORDER_URL + '/users_list',
}

interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

interface UsersDataResponse {
data: UserDataType[];
recordsTotal: number;
}

const fetchUsers = async (countryCode: string, start: number, length: number) => {
const url = new URL(PROXY_URLS[countryCode]);
url.searchParams.append('start', start.toString());
url.searchParams.append('length', length.toString());
const response = await this.get<UsersDataResponse>(url.toString());

return response;
};

2. The part of the application that fetches a specific customer record should undergo the following modifications:

Demo application before integration with the InCountry platform:

const APPLICATION_BACKEND_URL = 'https://app-backend-url.com/user';

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const fetchUser = async (id: string) => {
const url = new URL(APPLICATION_BACKEND_URL + `/${id}`);
const response = await this.get<UserDataType>(url.toString());

return response;
};

Demo application after integration with the InCountry platform:

const CN_BORDER_URL = 'https://cn-proxy-mt-01.alicloud.zhaoerxing.top/x-inc-demoapp';
const SA_BORDER_URL = 'https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp';
const US_BORDER_URL = 'https://us-proxy-mt-01.uat.incountry.io/x-inc-demoapp';

const PROXY_URLS = {
cn: CN_BORDER_URL + '/user',
sa: SA_BORDER_URL + '/user',
us: US_BORDER_URL + '/user',
}

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const fetchUser = async (id: string, countryCode: string) => {
const url = new URL(PROXY_URLS[countryCode] + `/${id}`);
const response = await this.get<UserDataType>(url.toString());

return response;
};

Record update endpoints

Update of customer records is performed through the POST /update_users/:id endpoint.

The request payload for the POST /update_users/:id endpoint is similar to the following:

Request body

{
"fullName": "Harry Styles",
"email": "hstyles@yahoo.com",
"phone": "+8435093409",
"vehicle": "Toyota Corolla",
"yearlySpending": 456,
"country": "us"
}

The response body returned by the POST /update_users/:id endpoint is similar to the following:

Response body

{
"fullName": "Harry Styles",
"email": "hstyles@yahoo.com",
"phone": "+8435093409",
"vehicle": "Toyota Corolla",
"yearlySpending": 456,
"country": "us",
"id": "fh3hCDoGs"
}

Using this endpoint, the application backend fetches customer records by their id pulled from the request body and then updates this data with new values. So, the application backend on this endpoint takes a record by ID specified within the query and updates this record in the application database.

Since the record update endpoint usually requires a POST request, you need a redaction and an unredaction rule.

To redact data, create a redaction rule. The difference is that in this redaction rule you need to specify the application ID in the request payload not in the response contained with the entityIdPath field. However, in the demo application these paths are identical.

Border configuration - record update endpoint (redaction rule)

{
"collectionName": "users",
"entityErrorCorrectionFieldPath": "$.email",
"entityIdPath": "$.id",
"globalEntityId": true,
"method": "POST",
"path": "/update_users/?(.+)",
"strategies": [
{
"path": "$.fullName",
"strategy": "alphaNumericPersistent",
},
{
"path": "$.email",
"strategy": "emailPersistent",
},
{
"path": "$.phone",
"strategy": "alphaNumeric"
},
{
"path": "$.yearlySpending",
"strategy": "numeric"
}
]
}

To unredact sensitive data, specify the following unredaction rule:

Border configuration - record update endpoint (unredaction rule)

{
"collections": [
{
"entityErrorCorrectionFieldPath": "$.email",
"entityIdPath": "$.id",
"globalEntityId": true,
"name": "users",
"strategies": [
{
"path": "$.name"
},
{
"isErrorCorrectionField": true,
"path": "$.email"
},
{
"path": "$.phone"
},
{
"path": "$.yearlySpending"
}
]
}
],
"method": "POST",
"path": "/update_users/?(.+)"
}

After saving the Border configuration, you need to proxy requests through the Border URL so records will be updated on the InCountry platform. As usual, if unredaction works correctly, the response from Border should include clear-text values just as in the original response, otherwise, values will be redacted (tokens/hashes).

Based on the user’s location and the record’s attribution to a specific country, the record update request is sent to one of the three Border instances in USA, China, or Saudi Arabia.

tip

To integrate the record update endpoint in your application:

  1. Add a redaction rule for the record update endpoint in the Border configuration. Ensure that you set the correct entityIdPath.

  2. Add an unredaction rule in the Border configuration.

  3. Switch the record update endpoint from the application backend URL to Border URL.

Demo application before integration with the InCountry platform:

const APPLICATION_BACKEND_URL = 'https://app-backend-url.com/update_users';

interface RequestDataType {
fullName: string;
email: string;
phone: string;
vehicle: string;
yearlySpending: number;
country: string;
}

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const updateUser = async (id: string, data: RequestDataType) => {
const url = new URL(APPLICATION_BACKEND_URL + `/${id}`);
const response = await this.post<RequestDataType, UserDataType>(url.toString(), data);

return response;
};

Demo application after integration with the InCountry platform:

const CN_BORDER_URL = 'https://cn-proxy-mt-01.alicloud.zhaoerxing.top/x-inc-demoapp';
const SA_BORDER_URL = 'https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp';
const US_BORDER_URL = 'https://us-proxy-mt-01.uat.incountry.io/x-inc-demoapp';

const PROXY_URLS = {
cn: CN_BORDER_URL + '/update_users',
sa: SA_BORDER_URL + '/update_users',
us: US_BORDER_URL + '/update_users',
}

interface RequestDataType {
fullName: string;
email: string;
phone: string;
vehicle: string;
yearlySpending: number;
country: string;
}

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const updateUser = async (data: RequestDataType) => {
const url = new URL(PROXY_URLS[data.country] + `/${id}`);
const response = await post<RequestDataType, UserDataType>(url.toString(), data);

return response;
};

Here post is some method async post<T, U>(url: string, data: T) which sends authenticated POST request to url with T type request body and responds with U type response body.

Record deletion endpoint

Removal of customer records is performed through the POST /delete_users/:id endpoint. It takes neither request payload nor returns any response body.

Using this endpoint, the application backend deletes customer records by their id passed within the path parameter.

However, you need to create a redaction rule for it to remove the record on the InCountry platform (when deleting a record from the demo application) in the following way:

Border configuration - record delete endpoint (redaction rule)

{
"collectionName": "users",
"entityIdPath": "$.id",
"globalEntityId": true,
"isDeleteRequest": true,
"method": "POST",
"path": "/delete_users/?(.+)"
}

The new field here is isDeleteRequest which should be specified for delete requests. Since the entity id is not available in the request body and named query parameters, so the entityIdPath field is not required here either. Here Border takes the application ID from the path - it can do this if the path is the correct regular expression like in the example. But one could still use request body for deletion and there entityIdPath will be used in the same way as for the endpoints above.

The globalEntityId field in the demo application is set as true because all application IDs are autogenerated UUID values suitable for primary IDs. You may need to create an unredaction rule for this endpoint if there is a response body for a request on the application backend including regulated fields.

Based on the country where the delete request was initiated, the request is routed through one of the three Border instances (USA, China, or Saudi Arabia).

tip

To integrate the record deletion endpoint in your application, please do the following:

  1. Add a redaction rule for the record deletion endpoint in the Border configuration with the isDeleteRequest: true flag.

  2. Add an unredaction rule if the record deletion endpoint should be handled by Border in case if some regulated data is returned back.

  3. Switch the record update endpoint from the application backend URL to Border URL.

Demo application before integration with the InCountry platform:

const APPLICATION_BACKEND_URL = 'https://app-backend-url.com/delete_users';

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const deleteUser = async (id: string) => {
const url = new URL(APPLICATION_BACKEND_URL + `/${id}`);
const response = await this.post<{}, UserDataType>(url.toString(), {});

return response;
};

Demo application after integration with the InCountry platform:

const CN_BORDER_URL = 'https://cn-proxy-mt-01.alicloud.zhaoerxing.top/x-inc-demoapp';
const SA_BORDER_URL = 'https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp';
const US_BORDER_URL = 'https://us-proxy-mt-01.uat.incountry.io/x-inc-demoapp';

const PROXY_URLS = {
cn: CN_BORDER_URL + '/delete_users',
sa: SA_BORDER_URL + '/delete_users',
us: US_BORDER_URL + '/delete_users',
}

export interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const deleteUser = async (countryCode: string, id: string) => {
const url = new URL(PROXY_URLS[countryCode] + `/${id}`);
const response = await this.post<{}, UserDataType>(url.toString(), {});

return response;
};

Here post is some method async post<T, U>(url: string, data: T) which sends authenticated POST request to url with T type request body and responds with U type response body.

Search Integration

The search functionality is not supported by Border. The demo application is using the dedicated search endpoint in REST API to handle search queries coming from the demo application.

The search protocol works in the following way:

  1. The demo application backend and REST API generate a common AES secret at the /sf/service/generate-key endpoint, and it is kept in the demo application. Requests of such type are signed with a common pre-defined private keys.

  2. The demo application frontend requests a JWT token.

  3. The demo application backend issues a JWT token using a private key for the dedicated REST API’s endpoint (find IDs).

  4. The demo application frontend acquires this JWT token.

  5. Using the JWT token, demo application frontend makes a request to REST API with the search filter at the /sf/records/find-ids endpoint.

  6. REST API handles the passed filter and returns an encrypted list of record IDs that satisfy the search criteria.

  7. The demo application frontend sends encrypted IDs and a filter to the demo application backend.

  8. The demo application backend decrypts data, filters the list with record IDs according to its Access Control List (if exists).

  9. The demo application backend forms a request body based on the filtered IDs for the REST API’s main /find endpoint. It also generates a JWT token for this request.

  10. The demo application backend sends the request body and the token to the demo application frontend.

  11. The demo application frontend sends a request to main Rest API endpoint /sf/records/find and retrieves clear-text values of requested records.

Moreover, search should be aligned with CRUD operations. : The Salesforce service must be created within the same environment where Border configurations were configured on the InCountry Portal. In particular, for each country, you need to use the same environment which Border uses and the same encryption keys Border uses.

In the demo application, the partial-text match search is implemented for the Full Name and Email fields and the full-text match search for the Phone field. When saving customer records to the InCountry platform, the Full Name, Email, and Phone fields are saved to the special searchable keys: key1, key2 and key3 respectively. A search request in the demo application looks as follows:

{
country: "us",
filter: { $or: [{
key1: { $like: data.search.value }
}, {
key2: { $like: data.search.value }
}, {
key3: data.search.value
}]
}
}

Here the $or and $like operators regulate how the search engine works. The filter can be translated in the following: key1 should be like data.search.value OR key2 should be like data.search.value OR key3 should be equal to data.search.value. You can use additional search operators in the Exact Match Search and Partial Text Match Search sections.

The demo application deals with two countries: one country uses the restricted model and the other country uses the redacted model so we have two REST API instances (Saudi Arabia and China) where we will send requests for search.

In the replicated model, the record is stored in the application database in the clear-text form so we can query it directly from the application database.

tip

To integrate this search protocol to application, you need:

  1. To create a Salesforce service on InCountry Portal.

  2. Generate credentials for each REST API instance you plan to use (for each country).

  3. Generate certificate and private key for digital signature (e.g. RSA certificate and key using OpenSSL tools), pre-set the private key into the application and load the certificate to your Salesforce integration on Portal

  4. Upon the application start, the Application Backend needs to acquire an AES encryption key from REST API.

  5. On each search, run the protocol described above.

  6. Implement the List View Filters search protocol in your Application (requires code changes again) for redacted and restricted models.

  7. Implement the generic search endpoint on in your application backend for the replicated model.

The following code snippets demonstrate how you can search for records at the application frontend (implemented with React) by performing requests to the application backend and REST API. The partial text match search can be executed against the Full Name and Email fields (key1 and key2 in InCountry record) and the full text match search against the Phone field (key3 in InCountry record).

Demo application before integration with the InCountry platform:

const APPLICATION_BACKEND_URL = 'https://app-backend-url.com/find';

interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const searchEntries = async (filter: string) => {
const url = new URL(APPLICATION_BACKEND_URL);
const data = {
"filter": filter,
"options": { "limit": 100, "offset": 0 },
}
const response = await post<{}, UserDataType[]>(url.toString(), data);

return response;
};

Demo application after integration with the InCountry platform:

const CN_RESTAPI_URL = 'https://cn-restapi-mt-01.alicloud.zhaoerxing.top';
const SA_RESTAPI_URL = 'https://sa-restapi-mt-01.uat.incountry.io';
const US_RESTAPI_URL = 'https://us-restapi-mt-01.uat.incountry.io';
const APPLICATION_BACKEND_URL = 'https://app-backend-url.com';

const RESTAPI_URLS = {
cn: {
ids: CN_RESTAPI_URL + '/sf/records/find-ids',
acl: APPLICATION_BACKEND_URL + '/check_acl',
find: CN_RESTAPI_URL + '/find',
},
sa: {
ids: SA_RESTAPI_URL + '/sf/records/find-ids',
acl: APPLICATION_BACKEND_URL + '/check_acl',
find: SA_RESTAPI_URL + '/find',
},
us: {
find: APPLICATION_BACKEND_URL + '/find',
}
}

interface RequestDataType {
fullName: string;
email: string;
phone: string;
vehicle: string;
yearlySpending: number;
country: string;
}

interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

interface CheckAclResponse {
ids: string[];
body: string;
jwt_token: string;
}

interface FindResponse {
payload: {
fullName: string;
email: string;
phone: string;
yearlySpending: number;
};
record_key: string;
profile_key: string;
key1: string;
key2: string;
key3: string;
}

const findIds = async (countryCode: string, query: string) => {
const jwtToken = await getJWTTokenSomehow();
const url = new URL(RESTAPI_URLS[countryCode].ids);
const data = {
country: countryCode,
filter: { "$or": [
{ "key1": { "$like": query } },
{ "key2": { "$like": query } },
{ "key3": query }
] }
}
const response = await this.post<{ country: string, filter: unknown }, { result: string }>(
url.toString(),
data,
{ headers: { Authorization: `Bearer ${jwtToken}` } },
);

return response?.result;
};

public checkAcl = async (countryCode: string, findIdsResult: string) => {
const url = new URL(RESTAPI_URLS[countryCode].acl);
const data = { country: countryCode, findIdsResult };
const response = await post<{ country: string }, CheckAclResponse>(
url.toString(),
data,
);

return response;
};

public find = async (countryCode: string, profileKeys: string[], token: string) => {
const url = new URL(RESTAPI_URLS[countryCode].find);
const data = {
"country": countryCode,
"filter": { "profile_key": profileKeys },
"options": { "limit": 100, "offset": 0 },
"fields": ["fullName", "email", "phone", "yearlySpending"],
"returnKeys": ["record_key", "profile_key", "key1", "key2", "key3"]
}
const response = await post<{ country: string }, FindResponse[]>(url.toString(), data, { headers: { Authorization: `Bearer ${token}` } });

return response;
};

// redacted and restricted modes (CN and SA)
const searchEntries = async (countryCode: string, filter: string) => {
const ids = await findIds(countryCode, query);
if (!ids) return [];
const acl = await checkAcl(countryCode, ids);
if (!acl || acl?.ids?.length === 0) return [];
return find(countryCode, acl.ids, acl.jwt_token);
}

// replicated mode (US)
interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

const searchEntriesUS = async (query: string) => {
const url = new URL(RESTAPI_URLS['us'].find);
const data = { filter: query }
const response = await this.post<{ filter: string }, UserDataType[]>(url.toString(), data);

return response;
}

Here post is some method async post<T, U>(url: string, data: T, options?: RequestInit) which sends authenticated POST request to url with T type request body and responds with U type response body.

To fetch JWT tokens for REST API authentication (getJWTTokenSomehow method), you can use a special endpoint getJwtToken on the demo application backend.

Here {uppercase country code}_AES_SECRET is a secret which used to generate a common AES key (for the corresponding REST API instance), {uppercase country code}_PRIVATE_KEY is a private key that corresponds to the certificate uploaded to the dedicated Salesforce service on the InCountry Portal, and {uppercase country code}_SUBJECT_CLAIM is the subject claim that identifies a specific service (similar to tenant ID).

JWT token management at the Application Backend

const jwt = require('jsonwebtoken');

const getConfigData = (country) => {
if (country === 'us') {
return {
restapiUrl: process.env.US_RESTAPI_URL,
aesSecret: process.env.US_AES_SECRET,
privateKey: process.env.US_PRIVATE_KEY,
subjectClaim: process.env.US_SUBJECT_CLAIM,
}
}
if (country === 'cn') {
return {
restapiUrl: process.env.CN_RESTAPI_URL,
aesSecret: process.env.CN_AES_SECRET,
privateKey: process.env.CN_PRIVATE_KEY,
subjectClaim: process.env.CN_SUBJECT_CLAIM,
}
}
if (country === 'sa'') {
return {
restapiUrl: process.env.SA_RESTAPI_URL,
aesSecret: process.env.SA_AES_SECRET,
privateKey: process.env.SA_PRIVATE_KEY,
subjectClaim: process.env.SA_SUBJECT_CLAIM,
}
}
}

const getJwtToken = (req, res) => {
try {
validate(req.body);
} catch (e) {
res.status(400).send({ error: e.message });
return;
}

const { subjectClaim, aesSecret, privateKey } = getConfigData(req.body.country);

const payload = {
operation: 'findIds',
secret: aesSecret,
idField: 'profile_key',
exp: Math.floor((Date.now() + 15 * 60 * 1000) / 1000),
nbf: Math.floor(Date.now() / 1000),
iat: Math.floor(Date.now() / 1000),
"sub": subjectClaim
};

res.send({ jwt_token: jwt.sign(payload, privateKey, { algorithm: 'RS256' }) });
};

To generate an AES key common with REST API, during the demo application start you need to perform a request generating keys for each REST API instance, acquire the appropriate key, and save it to some storage like KEYS:

const jwt = require('jsonwebtoken');

const generateKeys = async () => {
for (const country of ['us', 'cn', 'sa']) {
const { restapiUrl, aesSecret, privateKey, subjectClaim } = getConfigData(country);

const token = jwt.sign({
operation: 'generateKey',
secret: aesSecret,
exp: Math.floor((Date.now() + 15 * 60 * 1000) / 1000),
nbf: Math.floor(Date.now() / 1000),
iat: Math.floor(Date.now() / 1000),
sub: subjectClaim,
}, privateKey, { algorithm: 'RS256' });

try {
const result = await axios.post(`${restapiUrl}/sf/service/generate-key`, {
secret: aesSecret,
},{
headers: { Authorization: `Bearer ${token}` }
});
KEYS[country] = result.data.key;
} catch (e) {
console.log(`Cannot correctly initialize application, error is ${e.message}`);
process.exit(1)
}
}

This common AES key should be used at the checkAcl endpoint (Access Control List check) which you need to implement as well. This key is used to decrypt the results returned by the findIds method (see the code snippets above):

Checking ACL at the application backend

const checkAcl = async (req, res) => {
try {
validate(req.body);
} catch (e) {
res.status(400).send({ error: e.message });
return;
}

const aesKey = KEYS[req.body.country];

const { findIdsResult } = req.body;

const [iv, encrypted] = findIdsResult.split(':');

const decrypted = decryptAes(encrypted, iv, aesKey);

const { ids, meta } = JSON.parse(decrypted);

const resultUserIds = filterUserIdsByApplicationACL(ids, req.body.country);
const resultBody = buildBody(resultUserIds, meta, req.body.country);

const { subjectClaim, privateKey } = getConfigData(req.body.country);

const jwtPayload = {
country: req.body.country,
operation: 'findRecords',
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000),
hash: calcHash(resultBody),
exp: Math.floor((Date.now() + 15 * 60 * 1000) / 1000),
sub: subjectClaim,
};

res.send({ ids: resultUserIds, body: resultBody, jwt_token: getJwtToken(jwtPayload, privateKey) });
};

Forming the search request body

const buildBody = (resultIds, meta, country) => {
return JSON.stringify({
country,
filter: {
profile_key: resultIds,
},
options: {
limit: meta.limit,
offset: meta.offset
},
fields: [
"fullName",
"email",
"phone",
"yearlySpending"
],
returnKeys: [
"record_key",
"profile_key",
"key1",
"key2",
"key3",
]
});
}

The filterUserIdsByApplicationACL method filters records according to ACL and must be implemented in your application based on the accepted Access Control List policy.

Integrating resident functions

You can use resident functions to validate regulated and sensitive data in the demo application. Resident functions are a part of the InCountry platform that allow you to execute JavaScript code against the protected data so it does not leave its country of origin.

To get started with resident functions, you need to set up a REST API integration which uses the same environment that Border works with. The demo application is using the same environment, so credentials for REST API and Border services can properly work with the same records storing in the same environment.

To execute a resident function, please do the following:

  1. Publish a resident function on the InCountry Portal. Please check our documentation.

  2. Integrate the resident function into your application and execute it at the /serverless/execute endpoint. To perform such requests, you need to authorize them with OAuth2 authentication. We recommend that you implement a special authorization endpoint on the application backend which passes a valid OAuth2 token to the application frontend. Having this OAuth2 token, the application frontend can call a resident function.

The demo application uses two resident functions:

  1. a resident function to validate the uniqueness of the entered email for each customer record. If there is a customer record already using such email, then the record creation or update is interrupted.

  2. a resident function to calculate the next year expenses on the customer details page.

tip

To authenticate through OAuth2, a dedicated authorization endpoint is implemented on the demo application backend which responds with a valid OAuth2 token.

While the demo application works with multiple countries (and implements different data regulation models), you need to publish and execute a resident function against each country (Saudi Arabia, China, and United States). Based on the selected country or user country, a request is performed to one of these three REST API instances.

tip

To execute a resident function, please do the following:

  1. Create a Resident Function service for each country used, this integration should use the same environment and country as the Border configuration uses.

  2. Publish a resident function on the InCountry Portal. Please check our documentation.

  3. Implement the OAuth2 authorization endpoint on the application backend which returns a valid OAuth2 token to each user authenticated in the application.

  4. Integrate the resident function into your application and execute it at the /serverless/execute endpoint. To perform such requests, you need to authorize them with OAuth2 authentication. We recommend that you implement a special authorization endpoint on the application backend which passes a valid OAuth2 token to the application frontend. Having this OAuth2 token, the application frontend can call a resident function.

  5. Make a request to call a resident function with the acquired OAuth2 token as an authorization token.

  6. Use the output of a resident function in the application.

We want to forbid the creation of a new customer record with the same email. First, you need to publish resident function which checks if there is a customer with the provided email so a new customer record can be created, for example:

Resident function checking the customer record duplicates

curl --request POST \
--url https://sa-restapi-mt-01.uat.incountry.io/serverless/publish \
--header 'Authorization: Bearer <InCountry OAuth2 token here>' \
--header 'Content-Type: application/json' \
--data '{
"scriptName": "demo-validation",
"scriptBody": "module.exports.handler = async (storage, country, params, modules) => { const { data } = await modules.axios.get('\''https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp/users'\''); return !data.filter((x) => x.country === '\''sa'\'').some((x) => x.email.toUpperCase() === params.email.toUpperCase()); };",
"options": {
"country": "sa",
"forceUpdate": true
}
}'

Then, before creating a new customer record, you can execute this function and use its result, as follows:

Calling the resident function

const CN_BORDER_URL = 'https://cn-proxy-mt-01.alicloud.zhaoerxing.top/x-inc-demoapp';
const SA_BORDER_URL = 'https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp';
const US_BORDER_URL = 'https://us-proxy-mt-01.uat.incountry.io/x-inc-demoapp';

const CN_RESTAPI_URL = 'https://cn-restapi-mt-01.alicloud.zhaoerxing.top';
const SA_RESTAPI_URL = 'https://sa-restapi-mt-01.uat.incountry.io';
const US_RESTAPI_URL = 'https://us-restapi-mt-01.uat.incountry.io';

const PROXY_URLS = {
cn: CN_BORDER_URL + '/users',
sa: SA_BORDER_URL + '/users',
us: US_BORDER_URL + '/users',
}

const RESTAPI_URLS = {
cn: CN_BORDER_URL + '/serverless/execute',
sa: SA_BORDER_URL + '/serverless/execute',
us: US_BORDER_URL + '/serverless/execute',
}

interface RequestDataType {
fullName: string;
email: string;
phone: string;
vehicle: string;
yearlySpending: number;
country: string;
}

interface UserDataType {
country: string;
email: string;
fullName: string;
id: string;
phone: string;
vehicle: string;
yearlySpending: number;
}

interface ResidentFunctionScriptParams {
email?: string;
}

interface ResidentFunctionExecuteRequestData {
scriptName: string;
options: {
country: string;
};
scriptParams: { [key: string]: string };
}

interface ResidentFunctionExecuteResponseData {
exitCode?: number;
exitSignal?: string;
result?: string | number | boolean;
stdout?: string;
stderr?: string;
duration: number;
}

const executeResidentFunction = async (countryCode: string, scriptParams: ResidentFunctionScriptParams, token: string) => {
const data = {
scriptName: 'demo-validation',
options: { country: countryCode },
scriptParams,
}
const options = { headers: { Authorization: `Bearer ${token}` } };
const response = await post<ResidentFunctionExecuteRequestData, ResidentFunctionExecuteResponseData>(
RESTAPI_URLS?.[countryCode],
data,
options
);

return response;
}

const createUser = async (data: RequestDataType) => {
const token = await getOAuthTokenSomehow();
const executionResponse = await executeResidentFunction(data.country, { email: data.email }, token);
if (executionResponse.result === false) throw new Error('User with this email already exists.');
const url = new URL(PROXY_URLS[data.country]);
const response = await post<RequestDataType, UserDataType>(url.toString(), data);

return response;
};

Here post is some method async post<T, U>(url: string, data: T) which sends authenticated POST request to url with T type request body and responds with U type response body.

To acquire an OAuth token for authentication on REST API (getOAuthTokenSomehow), you need to implement a a new endpoint getOAuthToken at the application backend. Here, we use the following variables:

  • envId, clientId, clientSecret: credentials of the REST API service

  • oAuthUrl: OAuth2 server URL

  • popapiUrl: PoP API instance URL

  • restapiUrl: REST API instance URL

Acquiring an OAuth2 token at the application backend

const getConfigData = (country) => {
if (country === UNITED_STATES) {
return {
oAuthUrl: process.env.US_HYDRA_URL,
popapiUrl: process.env.US_POPAPI_URL,
restapiUrl: process.env.US_RESTAPI_URL,
envId: process.env.US_ENVIRONMENT_ID,
clientId: process.env.US_CLIENT_ID,
clientSecret: process.env.US_CLIENT_SECRET,
}
}
if (country === CHINA) {
return {
oAuthUrl: process.env.CN_HYDRA_URL,
popapiUrl: process.env.CN_POPAPI_URL,
restapiUrl: process.env.CN_RESTAPI_URL,
envId: process.env.CN_ENVIRONMENT_ID,
clientId: process.env.CN_CLIENT_ID,
clientSecret: process.env.CN_CLIENT_SECRET,
}
}
if (country === SAUDI_ARABIA) {
return {
oAuthUrl: process.env.SA_HYDRA_URL,
popapiUrl: process.env.SA_POPAPI_URL,
restapiUrl: process.env.SA_RESTAPI_URL,
envId: process.env.SA_ENVIRONMENT_ID,
clientId: process.env.SA_CLIENT_ID,
clientSecret: process.env.SA_CLIENT_SECRET,
}
}
}

const getOAuthToken = async (req, res) => {
try {
validate(req.body);
} catch (e) {
res.status(400).send({ error: e.message });
return;
}

const { oAuthUrl, restapiUrl, popapiUrl, envId, clientId, clientSecret } = getConfigData(req.body.country);

try {
const result = await axios.post(oAuthUrl, qs.stringify({
grant_type: 'client_credentials',
scope: envId,
audience: `${restapiUrl} ${popapiUrl}`
}), {
auth: {
username: clientId,
password: clientSecret
}
});

res.send(result.data);
} catch (e) {
res.status(500).send({ error: e.message });
}
};

Below you can find the code of resident functions:

Checking duplicates of customer records

module.exports.handler = async (storage, country, params, modules) => {
const { data } = await modules.axios.get('https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp/users');
return !data.filter((x) => x.country === 'sa').some((x) => x.email.toUpperCase() === params.email.toUpperCase());
};

Calculating the next year spendings based on the current year values

module.exports.handler = async (storage, country, params, modules) => {
const { data } = await modules.axios.get(`https://sa-proxy-mt-01.uat.incountry.io/x-inc-demoapp/users/${params.id}`);
return data.yearlySpending * 1.15 ;
};