Source

index.ts

import axios, { AxiosInstance } from "axios";
import https from "https";

const cryptoPath = "/api/crypto";
const vaultPath = "/api/vault";
const protectionPath = "/api/protection";
const securePath = "/api/secure";

export type EncryptionMode =
  | "AES_GCM"
  | "CHACHA20_POLY1305"
  | "XCHACHA20_POLY1305"
  | "RSA_OAEP";

export type HashMode =
  | "BLAKE2B_256"
  | "BLAKE2B_512"
  | "BLAKE2S_256"
  | "BLAKE3_256"
  | "BLAKE3_512"
  | "SHA256"
  | "SHA512"
  | "SHA3_256"
  | "SHA3_512";

export interface CryptoEncryptInput {
  keyID: string;
  mode: EncryptionMode;
  plaintexts: any[];
}

export interface CryptoHMACInput {
  keyID: string;
  mode: HashMode;
  values: any[];
}

export interface CryptoEqualInput {
  hash: string;
  value: any;
}

export interface CryptoSignInput {
  keyID: string;
  values: any[];
}

export interface CryptoVerifyInput {
  signature: string;
  value: any;
}

export interface VaultStoreInput {
  vaultID: string;
  values: any[];
}

export interface VaultFetchInput {
  vaultID: string;
  search: any;
  size?: number;
  after?: string;
}

export interface VaultCountInput {
  vaultID: string;
  search: any;
}

export interface VaultGetInput {
  vaultID: string;
  tokens: string[];
}

export interface VaultUpdateInput {
  vaultID: string;
  values: { token: string; value: any }[];
}

export interface VaultDeleteInput {
  vaultID: string;
  tokens: string[];
}

export interface ProtectionTokenizeInput {
  protectionID: string;
  values: any[];
}

export interface ProtectionSealInput {
  protectionID: string;
  primaryKeys: any[];
}

export interface ProtectionOpenInput {
  protectionID: string;
  tokens: any[];
}

export interface ProtectionFetchInput {
  protectionID: string;
  search: any;
}

export interface ProtectionCountInput {
  protectionID: string;
  search: any;
}

/**
 * @class
 * Create a new Kastela Client instance for communicating with the server.
 * Require server information and return client instance.
 * @param {string} kastelaUrl Kastela server url
 * @param {string} caCert Kastela ca certificate
 * @param {string} clientCert Kastela client certificate
 * @param {string} clientKey kastela client key
 */
export class Client {
  #axiosInstance: AxiosInstance;
  #kastelaURL: string;

  public constructor(
    kastelaURL: string,
    caCert: Buffer,
    clientCert: Buffer,
    clientKey: Buffer
  ) {
    this.#kastelaURL = kastelaURL;
    const httpsAgent = new https.Agent({
      ca: caCert,
      cert: clientCert,
      key: clientKey,
    });
    this.#axiosInstance = axios.create({
      httpsAgent,
    });
  }

  async #request(method: string, url: URL, body?: any) {
    try {
      const { data } = await this.#axiosInstance.request({
        url: url.toString(),
        method,
        data: body,
      });
      return data;
    } catch (error: any) {
      const data = error?.response?.data;
      if (data) {
        switch (typeof data) {
          case "object":
            throw new Error(data.error);
          default:
            throw new Error(data);
        }
      } else {
        throw error;
      }
    }
  }

  /** Encrypt data
   * @param {Object[]} input input
   * @param {string} input[].keyID key id
   * @param {string} input[].mode encryption mode
   * @param {any[]} input[].plaintexts array of plaintexts
   * @return {Promise<string[][]>} array of ciphertext. the order of ciphertext corresponds to the order of input
   * @example
   * const ciphertexts = await client.cryptoEncrypt([{keyID: "your-key-id", mode: "AES_GCM", plaintexts: ["foo", "bar"]}]);
   */
  public async cryptoEncrypt(input: CryptoEncryptInput[]): Promise<string[][]> {
    const { ciphertexts } = await this.#request(
      "POST",
      new URL(`${cryptoPath}/encrypt`, this.#kastelaURL),
      input.map((v) => ({
        key_id: v.keyID,
        mode: v.mode,
        plaintexts: v.plaintexts,
      }))
    );
    return ciphertexts;
  }

  /** Decrypt data
   * @param {string[]} input array of ciphertext
   * @return {Promise<any[]>} array of plaintext. the order of plaintext corresponds to the order of ciphertext
   * @example
   * const plaintexts = await client.cryptoDecrypt(["foo", "bar"]);
   */
  public async cryptoDecrypt(input: string[]): Promise<any[]> {
    const { plaintexts } = await this.#request(
      "POST",
      new URL(`${cryptoPath}/decrypt`, this.#kastelaURL),
      input
    );
    return plaintexts;
  }

  /** HMAC data
   * @param {Object[]} input input
   * @param {string} input[].keyID key id
   * @param {string} input[].mode hash mode
   * @param {any[]} input[].values array of value
   * @return {Promise<string[][]>} array of hash. the order of hash corresponds to the order of input
   * @example
   * const hashes = await client.cryptoHMAC([{keyID: "your-key-id", mode: "SHA256", values: ["foo", "bar"]}]);
   */
  public async cryptoHMAC(input: CryptoHMACInput[]): Promise<string[][]> {
    const { hashes } = await this.#request(
      "POST",
      new URL(`${cryptoPath}/hmac`, this.#kastelaURL),
      input.map((v) => ({
        key_id: v.keyID,
        mode: v.mode,
        values: v.values,
      }))
    );
    return hashes;
  }

  /** Compare hash and data
   * @param {Object[]} input input
   * @param {string} input[].hash hash
   * @param {any} input[].value value
   * @return {Promise<boolean[]>} array of result. the order of result corresponds to the order of input
   * @example
   * const result = await client.cryptoEqual([{hash: "foo", value: 123}, {hash: "bar", value: 456}]);
   */
  public async cryptoEqual(input: CryptoEqualInput[]): Promise<boolean[]> {
    const { result } = await this.#request(
      "POST",
      new URL(`${cryptoPath}/equal`, this.#kastelaURL),
      input
    );
    return result;
  }

  /** Sign data
   * @param {Object[]} input input
   * @param {string} input[].keyID key id
   * @param {any[]} input[].values array of value
   * @return {Promise<string[][]>} array of signature. the order of signature corresponds to the order of input
   * @example
   * const signatures = await client.cryptoSign([{keyID: "your-key-id", values: ["foo", "bar"]}]);
   */
  public async cryptoSign(input: CryptoSignInput[]): Promise<string[][]> {
    const { signatures } = await this.#request(
      "POST",
      new URL(`${cryptoPath}/sign`, this.#kastelaURL),
      input.map((v) => ({ key_id: v.keyID, values: v.values }))
    );
    return signatures;
  }

  /** Verify data signature
   * @param {Object[]} input input
   * @param {string} input[].signature hash
   * @param {any} input[].value value
   * @return {Promise<boolean[]>} array of result. the order of result corresponds to the order of input
   * @example
   * const result = await client.cryptoVerify([{signature: "foo", value: 123}, {signature: "bar", value: 456}]);
   */
  public async cryptoVerify(input: CryptoVerifyInput[]): Promise<boolean[]> {
    const { result } = await this.#request(
      "POST",
      new URL(`${cryptoPath}/verify`, this.#kastelaURL),
      input
    );
    return result;
  }

  /** Store vault data
   * @param {Object[]} input input
   * @param {string} input[].vaultID vault id
   * @param {any[]} input[].values array of vault data
   * @return {Promise<string[][]>} array of vault token. the order of token corresponds to the order of input
   * @example
   * const tokens = await client.vaultStore([{vaultID: "your-vault-id", values: ["foo", "bar"]}]);
   */
  public async vaultStore(input: VaultStoreInput[]): Promise<string[][]> {
    const { tokens } = await this.#request(
      "POST",
      new URL(`${vaultPath}/store`, this.#kastelaURL),
      input.map((v) => ({ vault_id: v.vaultID, values: v.values }))
    );
    return tokens;
  }

  /** Search vault data
   * @param {Object} input input
   * @param {string} input.vaultID vault id
   * @param {string} input.search data to search
   * @return {Promise<string[]>}
   * @example
   * const tokens = await client.vaultFetch({vaultID: "your-vault-id", search: "foo", size: 10, after: "bar"})
   */
  public async vaultFetch(input: VaultFetchInput): Promise<string[]> {
    const body: {
      vault_id: string;
      search: any;
      size?: number;
      after?: string;
    } = { vault_id: input.vaultID, search: input.search };
    if (input.size) {
      body.size = input.size;
    }
    if (input.after?.length) {
      body.after = input.after;
    }
    const { tokens } = await this.#request(
      "POST",
      new URL(`${vaultPath}/fetch`, this.#kastelaURL),
      body
    );
    return tokens;
  }

  /** Count vault data
   * @param {Object} input input
   * @param {string} input.vaultID vault id
   * @param {string} input.search data to search
   * @return {Promise<string[]>}
   * @example
   * const count = await client.vaultCount({vaultID: "your-vault-id", search: "foo"})
   */
  public async vaultCount(input: VaultCountInput): Promise<number> {
    const body: { vault_id: string; search: any } = {
      vault_id: input.vaultID,
      search: input.search,
    };
    const { count } = await this.#request(
      "POST",
      new URL(`${vaultPath}/count`, this.#kastelaURL),
      body
    );
    return count;
  }

  /** Get vault data
   * @param {Object[]} input
   * @param {string} input[].vaultID vault id
   * @param {string[]} input[].tokens array of token
   * @return {Promise<any[][]>} array of value, the order of value corresponds to the order of token
   * @example
   * const values = await client.VaultGet([{vaultID: "your-vault-id", tokens: ["foo", "bar", "baz"]}]);
   */
  public async vaultGet(input: VaultGetInput[]): Promise<any[][]> {
    const { values } = await this.#request(
      "POST",
      new URL(`${vaultPath}/get`, this.#kastelaURL),
      input.map((v) => ({ vault_id: v.vaultID, tokens: v.tokens }))
    );
    return values;
  }

  /** Update vault data
   * @param {Object[]} input
   * @param {string} input[].vaultID vault id
   * @param {Object[]} input[].values array of values
   * @param {string} input[].values[].token token
   * @param {any} input[].values[].value value
   * @return {Promise<void>}
   * @example
   * await client.vaultUpdate([{vaultID: "your-vault-id", values: [{ token: "foo", value: "bar"}]}])
   */
  public async vaultUpdate(input: VaultUpdateInput[]): Promise<void> {
    await this.#request(
      "POST",
      new URL(`${vaultPath}/update`, this.#kastelaURL),
      input.map((v) => ({ vault_id: v.vaultID, values: v.values }))
    );
  }

  /** Delete vault data
   * @param {Object[]} input
   * @param {string} input[].vaultID vault id
   * @param {string[]} input[].tokens array of tokens
   * @return {Promise<void>}
   * @example
   * await client.vaultDelete([{ vaultID: "your-vault-id", tokens: ["foo", "bar"]}])
   */
  public async vaultDelete(input: VaultDeleteInput[]): Promise<void> {
    await this.#request(
      "POST",
      new URL(`${vaultPath}/delete`, this.#kastelaURL),
      input.map((v) => ({ vault_id: v.vaultID, tokens: v.tokens }))
    );
  }

  /** Tokenize data for protection
   * @param {Object[]} input protection tokenize input data
   * @param {string} input[].protectionID protection id
   * @param {any[]} input[].values array of data
   * @return {Promise<void>}
   * @example
   * const tokens = await client.protectionTokenize([{ protectionID: "your-protection-id", values: ["foo", "bar", "baz"]}])
   */
  public async protectionTokenize(
    input: ProtectionTokenizeInput[]
  ): Promise<any[][]> {
    const { tokens } = await this.#request(
      "POST",
      new URL(`${protectionPath}/tokenize`, this.#kastelaURL),
      input.map((v) => ({
        protection_id: v.protectionID,
        values: v.values,
      }))
    );
    return tokens;
  }

  /** Encrypt protection data
   * @param {Object[]} input protection seal input data
   * @param {string} input[].protectionID protection id
   * @param {any[]} input[].primaryKeys array of data primary keys
   * @return {Promise<void>}
   * @example
   * await client.protectionSeal([{ protectionID: "your-protection-id", primaryKeys: [1, 2, 3]}])
   */
  public async protectionSeal(input: ProtectionSealInput[]): Promise<void> {
    await this.#request(
      "POST",
      new URL(`${protectionPath}/seal`, this.#kastelaURL),
      input.map((v) => ({
        protection_id: v.protectionID,
        primary_keys: v.primaryKeys,
      }))
    );
  }

  /** Decrypt protection data
   * @param {Object[]} input protection open input data
   * @param {string} input[].protectionID protection id
   * @param {any[]} input[].tokens array of tokens
   * @return {Promise<any[][]>} array of decrypted data. the order of values corresponds to the order of input.
   * @example
   * const data = await client.protectionOpen([{ protectionID: "your-protection-id", tokens: ["foo", "bar", "baz"]}])
   */
  public async protectionOpen(input: ProtectionOpenInput[]): Promise<any[][]> {
    const { values } = await this.#request(
      "POST",
      new URL(`${protectionPath}/open`, this.#kastelaURL),
      input.map((v) => ({ protection_id: v.protectionID, tokens: v.tokens }))
    );
    return values;
  }

  /** Fetch protection data
   * @param {Object} input protection fetch input data
   * @param {string} input.protectionID protection id
   * @param {any} input.search data to search
   * @return {Promise<any[]>} array of primary keys
   * @example
   * const primaryKeys = await client.protectionFetch({ protectionID: "your-protection-id", search: "foo"})
   */
  public async protectionFetch(input: ProtectionFetchInput): Promise<any[]> {
    const { primary_keys } = await this.#request(
      "POST",
      new URL(`${protectionPath}/fetch`, this.#kastelaURL),
      { protection_id: input.protectionID, search: input.search }
    );
    return primary_keys;
  }

  /** Count protection data
   * @param {Object} input protection count input data
   * @param {string} input.protectionID protection id
   * @param {any} input.search data to search
   * @return {Promise<number>} count of data
   * @example
   * const count = await client.protectionCount({ protectionID: "your-protection-id", search: "foo"})
   */
  public async protectionCount(input: ProtectionCountInput): Promise<number> {
    const { count } = await this.#request(
      "POST",
      new URL(`${protectionPath}/count`, this.#kastelaURL),
      { protection_id: input.protectionID, search: input.search }
    );
    return count;
  }

  /** Initialize secure protection.
   * @param {string} operation secure protection operation mode
   * @param {string[]} protectionIDs array of protection id
   * @param {number} ttl time to live in minutes
   * @return {Promise<{ credential: string}>} secure protection credential
   * @example
   * const { credential } = await client.secureProtectionInit(["your-protection-id"], 5)
   */
  public async secureProtectionInit(
    operation: "READ" | "WRITE",
    protectionIDs: string[],
    ttl: number
  ): Promise<{ credential: string }> {
    const { credential } = await this.#request(
      "POST",
      new URL(`${securePath}/protection/init`, this.#kastelaURL),
      {
        operation,
        protection_ids: protectionIDs,
        ttl: ttl,
      }
    );
    return { credential };
  }

  /** Commit secure protection.
   * @param {string} credential
   * @return {Promise<void>}
   * @example
   * await client.secureProtectionCommit("your-credential")
   */
  public async secureProtectionCommit(credential: string): Promise<void> {
    await this.#request(
      "POST",
      new URL(`${securePath}/protection/commit`, this.#kastelaURL),
      { credential }
    );
  }

  /** Initialize secure vault.
   * @param {string} operation secure vault operation mode
   * @param {string[]} vaultIDs array of vault id
   * @param {number} ttl time to live in minutes
   * @return {Promise<{ credential: string}>} secure vault credential
   * @example
   * const { credential } = await client.secureVaultInit(["your-vault-id"], 5)
   */
  public async secureVaultInit(
    operation: "READ" | "WRITE",
    vaultIDs: string[],
    ttl: number
  ): Promise<{ credential: string }> {
    const { credential } = await this.#request(
      "POST",
      new URL(`${securePath}/vault/init`, this.#kastelaURL),
      {
        operation,
        vault_ids: vaultIDs,
        ttl: ttl,
      }
    );
    return { credential };
  }

  /**
   *  proxying your request.
   * @param {"json"|"xml"} type request body type
   * @param {string} url request url
   * @param {"get"|"post"} param.method request method
   * @param {object} [common] needed information for protection and vault.
   * @param {object} [common.vaults] vaults object list. Define column with prefix as key and array with id as first index and vault column as second index.
   * @param {object} [common.protections] protections object list. Define column with prefix as key and protectionId as value.
   * @param {object} [options.headers] request headers, use "_" prefix for encrypted column key and data id/token as value.
   * @param {object} [options.params] request parameters, use "_" prefix for encrypted column key and data id/token as value.
   * @param {object} [options.body] request body, use "_" prefix for encrypted column key and data id/token as value.
   * @param {object} [options.query] request query, use "_" prefix for encrypted column key and data id/token as value.
   * @param {string} [options.rootTag] root tag, required for xml type
   * @return {Promise<any>}
   * @example
   *client.privacyProxyRequest(
      "json",
      "https://enskbwhbhec7l.x.pipedream.net/:_phone/:_salary",
      "post",
      {
        protections: {
          _email: "124edec8-530e-4fd2-a04b-d4dc21ce625a", // email protection id
          _phone: "9f53aa3b-7214-436d-af9b-d2952be9f0c4", // phone protection id
        },
        vaults: {
          _salary: ["c5f9236d-aea0-46a5-a2fe-fb75c0596c87", "salary"], // salary vault id & column
        },
      },
      {
        headers: {
          _email: "1", // email data id
        },
        params: {
          _phone: "1", // email data id
          _salary: "01GQEATT1Q3NKKDC3A2JSMN7ZJ", // salary vault token
        },
        body: {
          name: "jhon daeng",
          _email: "1", // email data id
          _phone: "1", // phone data id
          _salary: "01GQEATT1Q3NKKDC3A2JSMN7ZJ", salary vault token
        },
        query: {
          id: "123456789",
          _email: "1",
        },
      }
    );
   */
  public async privacyProxyRequest(
    type: "json" | "xml",
    url: string,
    method: "get" | "post" | "put" | "delete" | "patch",
    common: {
      protections: object;
      vaults: object;
    } = { protections: {}, vaults: {} },
    options: {
      headers?: object;
      params?: object;
      body?: object;
      query?: object;
      rootTag?: string;
    } = {}
  ) {
    if (type === "xml" && !options.rootTag) {
      throw new Error("rootTag is required for xml");
    }
    const data = await this.#request(
      "POST",
      new URL(`/api/proxy`, this.#kastelaURL),
      {
        type,
        url,
        method,
        common,
        options,
      }
    );
    return data;
  }
}