From 7ade4a716149405e832bc65b34f2db280c884a5e Mon Sep 17 00:00:00 2001 From: Diego Diestra Date: Fri, 17 Oct 2025 10:03:38 -0500 Subject: [PATCH] chore: remove old sdk dependency add device info header --- demo/Collect.tsx | 7 +- package.json | 1 - src/components/CardNumberElement.hook.ts | 2 +- src/components/useCustomBin.hook.ts | 2 +- src/index.ts | 26 +- src/model/EncryptTokenData.ts | 14 +- src/modules/proxy.ts | 10 +- src/modules/sessions.ts | 4 +- src/modules/tokenIntents.ts | 8 +- src/modules/tokens.ts | 8 +- src/services/api/bt-client.ts | 317 ++++++++++++++++++ .../api/mappers/case-conversion-utils.ts | 100 ++++++ .../api/mappers/sessions/toSessionResponse.ts | 18 + .../api/mappers/sessions/type-guards.ts | 7 + .../token-intents/toTokenIntentRequest.ts | 15 + .../token-intents/toTokenIntentResponse.ts | 21 ++ .../api/mappers/token-intents/type-guards.ts | 10 + .../api/mappers/tokens/toTokenRequest.ts | 11 + .../api/mappers/tokens/toTokenResponse.ts | 34 ++ .../mappers/tokens/toUpdateTokenRequest.ts | 11 + .../api/mappers/tokens/type-guards.ts | 35 ++ src/services/api/proxy-client.ts | 279 +++++++++++++++ src/services/api/utils/nativeUtils.ts | 117 +++++++ src/services/basis-theory-js.ts | 106 ++++++ src/services/deviceInfo.ts | 39 +++ src/types/index.ts | 3 + src/types/models/card-types.ts | 216 ++++++++++++ src/types/models/device.ts | 47 +++ src/types/models/errors.ts | 23 ++ src/types/models/index.ts | 9 + src/types/models/proxy.ts | 13 + src/types/models/sessions.ts | 9 + src/types/models/shared.ts | 9 + src/types/models/token-intents.ts | 19 ++ src/types/models/tokenize.ts | 3 + src/types/models/tokens.ts | 53 +++ src/types/services/basis-theory.ts | 64 ++++ src/types/services/shared.ts | 8 + src/useBasisTheory.ts | 28 +- src/utils/shared.ts | 2 +- tests/components/CardNumberElement.test.tsx | 2 +- tests/modules/tokenEncryption.test.ts | 2 +- tests/modules/tokenIntents.test.ts | 2 +- tests/modules/tokens.test.ts | 2 +- tests/utils/dataManipulationUtils.test.ts | 2 +- yarn.lock | 106 +----- 46 files changed, 1668 insertions(+), 156 deletions(-) create mode 100644 src/services/api/bt-client.ts create mode 100644 src/services/api/mappers/case-conversion-utils.ts create mode 100644 src/services/api/mappers/sessions/toSessionResponse.ts create mode 100644 src/services/api/mappers/sessions/type-guards.ts create mode 100644 src/services/api/mappers/token-intents/toTokenIntentRequest.ts create mode 100644 src/services/api/mappers/token-intents/toTokenIntentResponse.ts create mode 100644 src/services/api/mappers/token-intents/type-guards.ts create mode 100644 src/services/api/mappers/tokens/toTokenRequest.ts create mode 100644 src/services/api/mappers/tokens/toTokenResponse.ts create mode 100644 src/services/api/mappers/tokens/toUpdateTokenRequest.ts create mode 100644 src/services/api/mappers/tokens/type-guards.ts create mode 100644 src/services/api/proxy-client.ts create mode 100644 src/services/api/utils/nativeUtils.ts create mode 100644 src/services/basis-theory-js.ts create mode 100644 src/services/deviceInfo.ts create mode 100644 src/types/index.ts create mode 100644 src/types/models/card-types.ts create mode 100644 src/types/models/device.ts create mode 100644 src/types/models/errors.ts create mode 100644 src/types/models/index.ts create mode 100644 src/types/models/proxy.ts create mode 100644 src/types/models/sessions.ts create mode 100644 src/types/models/shared.ts create mode 100644 src/types/models/token-intents.ts create mode 100644 src/types/models/tokenize.ts create mode 100644 src/types/models/tokens.ts create mode 100644 src/types/services/basis-theory.ts create mode 100644 src/types/services/shared.ts diff --git a/demo/Collect.tsx b/demo/Collect.tsx index c5781de..bc3d407 100644 --- a/demo/Collect.tsx +++ b/demo/Collect.tsx @@ -10,17 +10,13 @@ import { TextInput, View, } from 'react-native'; -import type { BTRef, BTDateRef, ElementEvent } from '../src'; +import type { BTRef, BTDateRef, ElementEvent, Token, TokenizeData } from '../src'; import { CardExpirationDateElement, CardNumberElement, CardVerificationCodeElement, useBasisTheory, } from '../src'; -import type { - Token, - TokenizeData, -} from '@basis-theory/basis-theory-js/types/models'; import { styles } from './styles'; import type { ElementEvents } from '../App'; import { EncryptedToken, EncryptToken } from '../src/model/EncryptTokenData'; @@ -314,3 +310,4 @@ export const Collect = () => { ); }; + diff --git a/package.json b/package.json index 0bff035..e1ffbb7 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "test": "jest" }, "dependencies": { - "@basis-theory/basis-theory-js": "^4.28.2", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.2", "@noble/hashes": "^1.8.0", diff --git a/src/components/CardNumberElement.hook.ts b/src/components/CardNumberElement.hook.ts index 31b06a0..24065f6 100644 --- a/src/components/CardNumberElement.hook.ts +++ b/src/components/CardNumberElement.hook.ts @@ -1,4 +1,4 @@ -import type { CreditCardType } from '@basis-theory/basis-theory-js/types/elements'; +import type { CreditCardType } from '../types'; import type { ForwardedRef } from 'react'; import { useId, useRef, useState } from 'react'; import type { TextInput } from 'react-native'; diff --git a/src/components/useCustomBin.hook.ts b/src/components/useCustomBin.hook.ts index 58dcf32..ca8a911 100644 --- a/src/components/useCustomBin.hook.ts +++ b/src/components/useCustomBin.hook.ts @@ -1,4 +1,4 @@ -import type { CreditCardType } from '@basis-theory/basis-theory-js/types/elements'; +import type { CreditCardType } from '../types'; import { creditCardType } from 'card-validator'; import { always, compose, groupBy, ifElse, isEmpty } from 'ramda'; import { useEffect, useState } from 'react'; diff --git a/src/index.ts b/src/index.ts index a456171..0fec97e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,4 +7,28 @@ export { TextElement } from './components/TextElement'; export { BasisTheoryProvider } from './BasisTheoryProvider'; export { useBasisTheory } from './useBasisTheory'; export type { BTRef, BTDateRef, ElementEvent } from './BaseElementTypes'; -export type { Token } from '@basis-theory/basis-theory-js/types/models'; +export type { + Token, + CreateToken, + UpdateToken, + TokenizeData, + CreateSessionResponse, + CreateTokenIntent, + TokenIntent, + RequestOptions, + DeviceInfo, + BasisTheoryInstance, + CreditCardType, + VISA, + MASTERCARD, + AMERICAN_EXPRESS, + DINERS_CLUB, + DISCOVER, + JCB, + UNIONPAY, + MAESTRO, + ELO, + MIR, + HIPER, + HIPERCARD, +} from './types'; diff --git a/src/model/EncryptTokenData.ts b/src/model/EncryptTokenData.ts index 53eec24..4c196fc 100644 --- a/src/model/EncryptTokenData.ts +++ b/src/model/EncryptTokenData.ts @@ -1,19 +1,17 @@ -// TODO: Migrate to web-elements when we completely drop basis-theory-js -import { - CreateToken, - Primitive, - TokenBase, -} from '@basis-theory/basis-theory-js/types/models'; +import type { CreateToken } from '../types'; import { BTRef, InputBTRefWithDatepart } from '../BaseElementTypes'; /** * Represents token data with element references for secure input handling. * Used when creating tokens that contain form element references instead of raw values. */ +// Primitive types +type Primitive = string | number | boolean | null | undefined; + type TokenDataWithRef = { /** Key-value pairs where values are element references or date part references */ data: Record; - type: TokenBase['type']; + type: string; }; type TokenData = Pick; @@ -44,7 +42,7 @@ type EncryptedSingleToken = { encrypted: string; /** Original token type before encryption */ - type: TokenBase['type']; + type: string; }; type EncryptedToken = EncryptedSingleToken | Record; diff --git a/src/modules/proxy.ts b/src/modules/proxy.ts index dc93652..c5f0c1e 100644 --- a/src/modules/proxy.ts +++ b/src/modules/proxy.ts @@ -1,18 +1,14 @@ -import type { BasisTheory } from '@basis-theory/basis-theory-js'; -import type { - ProxyRequestOptions, - BasisTheory as BasisTheoryType, -} from '@basis-theory/basis-theory-js/types/sdk'; +import type { ProxyRequestOptions, BasisTheoryInstance } from '../types'; import { replaceSensitiveData } from '../utils/dataManipulationUtils'; -export const Proxy = (bt: BasisTheoryType) => { +export const Proxy = (bt: BasisTheoryInstance) => { const proxy = async ( { method, ...proxyRequest }: Omit & { - method: keyof BasisTheory['proxy']; + method: keyof BasisTheoryInstance['proxy']; }, apiKey?: string ): Promise => { diff --git a/src/modules/sessions.ts b/src/modules/sessions.ts index 2ab378d..76f81a1 100644 --- a/src/modules/sessions.ts +++ b/src/modules/sessions.ts @@ -1,6 +1,6 @@ -import type { BasisTheory as BasisTheoryType } from '@basis-theory/basis-theory-js/types/sdk'; +import type { BasisTheoryInstance } from '../types'; -export const Sessions = (bt: BasisTheoryType) => { +export const Sessions = (bt: BasisTheoryInstance) => { const create = async () => { try { const session = await bt.sessions.create(); diff --git a/src/modules/tokenIntents.ts b/src/modules/tokenIntents.ts index f38249e..8057e1e 100644 --- a/src/modules/tokenIntents.ts +++ b/src/modules/tokenIntents.ts @@ -1,11 +1,9 @@ import type { CreateTokenIntent, TokenIntent, -} from '@basis-theory/basis-theory-js/types/models'; -import type { - BasisTheory as BasisTheoryType, RequestOptions, -} from '@basis-theory/basis-theory-js/types/sdk'; + BasisTheoryInstance, +} from '../types'; import type { BTRef, InputBTRefWithDatepart, @@ -23,7 +21,7 @@ export type TokenIntentData = TokenIntent< BTRef | InputBTRefWithDatepart | PrimitiveType >; -export const TokenIntents = (bt: BasisTheoryType) => { +export const TokenIntents = (bt: BasisTheoryInstance) => { const create = async ( tokenIntentWithRef: CreateTokenIntentWithBtRef, requestOptions?: RequestOptions diff --git a/src/modules/tokens.ts b/src/modules/tokens.ts index 105a82c..2951f13 100644 --- a/src/modules/tokens.ts +++ b/src/modules/tokens.ts @@ -3,11 +3,9 @@ import type { UpdateToken, Token, TokenizeData as _TokenizeData, -} from '@basis-theory/basis-theory-js/types/models'; -import type { - BasisTheory as BasisTheoryType, RequestOptions, -} from '@basis-theory/basis-theory-js/types/sdk'; + BasisTheoryInstance, +} from '../types'; import type { BTRef, InputBTRefWithDatepart, @@ -37,7 +35,7 @@ export type TokenizeData = _TokenizeData< BTRef | InputBTRefWithDatepart | PrimitiveType >; -export const Tokens = (bt: BasisTheoryType) => { +export const Tokens = (bt: BasisTheoryInstance) => { setupEncryption(); const getTokenById = async (id: string, apiKey?: string) => { diff --git a/src/services/api/bt-client.ts b/src/services/api/bt-client.ts new file mode 100644 index 0000000..409efcb --- /dev/null +++ b/src/services/api/bt-client.ts @@ -0,0 +1,317 @@ +import { + BasisTheoryApiError, + BasisTheoryValidationError, + CreateSessionResponse, + CreateToken, + CreateTokenIntent, + Token, + TokenIntent, + TokenizeData, + TokenizeDataModel, + DeviceInfo, + UpdateToken, +} from '../../types'; +import { getDeviceInfo } from '../deviceInfo'; +import { DeepTransformKeysCase } from './mappers/case-conversion-utils'; +import { toSessionResponse } from './mappers/sessions/toSessionResponse'; +import { toTokenIntentRequest } from './mappers/token-intents/toTokenIntentRequest'; +import { toTokenIntentResponse } from './mappers/token-intents/toTokenIntentResponse'; +import { toTokenRequest } from './mappers/tokens/toTokenRequest'; +import { toTokenResponse } from './mappers/tokens/toTokenResponse'; +import { toUpdateTokenRequest } from './mappers/tokens/toUpdateTokenRequest'; + +const API_KEY_HEADER = 'BT-API-KEY'; +const BT_TRACE_ID_HEADER = 'bt-trace-id'; +const BT_IDEMPOTENCY_KEY_HEADER = 'bt-idempotency-key'; +const CF_RAY = 'cf-ray'; +const BT_DEVICE_INFO_HEADER = 'BT-DEVICE-INFO'; + +type SnakeCaseToken = DeepTransformKeysCase; + +interface RequestOptions { + apiKey?: string; + correlationId?: string; + idempotencyKey?: string; + skipCaseConversion?: boolean; + deviceInfo?: DeviceInfo; +} + +interface BasisTheoryConfig { + apiKey?: string; + baseUrl: string; + debug?: boolean; +} + +const buildHeaders = ( + config: BasisTheoryConfig, + method: 'GET' | 'POST' | 'PATCH' | 'DELETE', + options?: RequestOptions +): Headers => { + const headers = new Headers(); + + // Set appropriate Content-Type based on method + if (method === 'PATCH') { + headers.set('Content-Type', 'application/merge-patch+json'); + } else if (method === 'POST') { + headers.set('Content-Type', 'application/json'); + } + // GET and DELETE typically don't need Content-Type + + const apiKey = options?.apiKey ?? config.apiKey; + if (apiKey) headers.set(API_KEY_HEADER, apiKey); + + if (options?.correlationId) { + headers.set(BT_TRACE_ID_HEADER, options.correlationId); + } + + if (options?.idempotencyKey) { + headers.set(BT_IDEMPOTENCY_KEY_HEADER, options.idempotencyKey); + } + + // Automatically collect and include device info + // Use provided deviceInfo or collect it automatically + const deviceInfo = options?.deviceInfo ?? getDeviceInfo(); + const data = JSON.stringify(deviceInfo); + // Use btoa for base64 encoding - available in React Native via polyfill + const base64 = typeof btoa !== 'undefined' + ? btoa(data) + : Buffer.from(data).toString('base64'); + headers.set(BT_DEVICE_INFO_HEADER, base64); + + return headers; +}; + +const makeRequest = async ( + config: BasisTheoryConfig, + endpoint: string, + method: 'GET' | 'POST' | 'PATCH' | 'DELETE', + body?: unknown, + options?: RequestOptions +): Promise => { + const headers = buildHeaders(config, method, options); + const url = `${config.baseUrl}${endpoint}`; + + const requestInit: RequestInit = { + method, + headers, + }; + + console.log('requestInit', requestInit); + + if (body && method !== 'GET') { + requestInit.body = JSON.stringify(body); + } + const response = await fetch(url, requestInit); + const data = await response.json(); + + if (!response.ok) { + if (response.status >= 400 && response.status < 500) { + // Client error - likely validation issue + if (data.errors && typeof data.errors === 'object') { + throw new BasisTheoryValidationError( + data.title || 'Validation failed', + data.errors, + [] // deprecated validation field + ); + } + throw new BasisTheoryApiError( + data.title || 'API request failed', + response.status, + data + ); + } + + throw new BasisTheoryApiError( + data.title || 'API request failed', + response.status, + data + ); + } + + const cfRay = response.headers.get(CF_RAY); + const btTraceId = response.headers.get(BT_TRACE_ID_HEADER); + + if (typeof data === 'string') { + return data as TResponse; + } + + return { + ...data, + ...{ _debug: config.debug ? { cfRay, btTraceId } : undefined }, + } as TResponse; +}; + +// Token service methods +const createToken = + (config: BasisTheoryConfig) => + async (payload: CreateToken, options?: RequestOptions): Promise => { + const _payload = toTokenRequest(payload); + + const response = await makeRequest( + config, + '/tokens', + 'POST', + _payload, + options + ); + + const tokenResponse = toTokenResponse(response); + + if (tokenResponse == undefined) { + throw new BasisTheoryApiError('Invalid API response. Try again.', -1); + } + + return tokenResponse; + }; + +const updateToken = + (config: BasisTheoryConfig) => + async ( + id: string, + payload: UpdateToken, + options?: RequestOptions + ): Promise => { + const _payload = toUpdateTokenRequest(payload); + + const response = await makeRequest( + config, + `/tokens/${id}`, + 'PATCH', + _payload, + options + ); + + const tokenResponse = toTokenResponse(response); + + if (tokenResponse == undefined) { + throw new BasisTheoryApiError('Invalid API response. Try again.', -1); + } + + return tokenResponse; + }; + +const retrieveToken = + (config: BasisTheoryConfig) => + async (id: string, options?: RequestOptions): Promise => { + const response = await makeRequest( + config, + `/tokens/${id}`, + 'GET', + undefined, + options + ); + + const tokenResponse = toTokenResponse(response); + + if (tokenResponse == undefined) { + throw new BasisTheoryApiError('Invalid API response. Try again.', -1); + } + + return tokenResponse; + }; + +const deleteToken = + (config: BasisTheoryConfig) => + async (id: string, options?: RequestOptions): Promise => { + await makeRequest( + config, + `/tokens/${id}`, + 'DELETE', + undefined, + options + ); + }; + +// Tokenization service +const tokenize = + (config: BasisTheoryConfig) => + ( + payload: TokenizeData, + options?: RequestOptions + ): Promise => + makeRequest( + config, + '/tokenize', + 'POST', + payload, + options + ); + +// Session service +const createSession = + (config: BasisTheoryConfig) => + async ( + payload?: unknown, + options?: RequestOptions + ): Promise => { + const result = await makeRequest< + DeepTransformKeysCase + >(config, '/sessions', 'POST', payload, options); + + const sessionResponse = toSessionResponse(result); + + if (sessionResponse == undefined) { + throw new BasisTheoryApiError('Invalid API response. Try again.', -1); + } + + return sessionResponse; + }; + +// Token intents service +const createTokenIntent = + (config: BasisTheoryConfig) => + async ( + payload: CreateTokenIntent, + options?: RequestOptions + ): Promise => { + const _payload = toTokenIntentRequest(payload); + + const result = await makeRequest< + DeepTransformKeysCase + >(config, '/token-intents', 'POST', _payload, options); + + const tokenIntent = toTokenIntentResponse(result); + + if (tokenIntent == undefined) { + throw new BasisTheoryApiError('Invalid API response. Try again.', -1); + } + + return tokenIntent; + }; + +const deleteTokenIntent = + (config: BasisTheoryConfig) => + async (id: string, options?: RequestOptions): Promise => { + await makeRequest( + config, + `/token-intents/${id}`, + 'DELETE', + undefined, + options + ); + }; + +// Create API service functions bound to config +const createBasisTheoryApi = (config: BasisTheoryConfig) => ({ + tokens: { + create: createToken(config), + update: updateToken(config), + retrieve: retrieveToken(config), + delete: deleteToken(config), + }, + tokenize: tokenize(config), + sessions: { + create: createSession(config), + }, + tokenIntents: { + create: createTokenIntent(config), + delete: deleteTokenIntent(config), + }, +}); + +export { + createBasisTheoryApi, + makeRequest, + type BasisTheoryConfig, + type RequestOptions, +}; diff --git a/src/services/api/mappers/case-conversion-utils.ts b/src/services/api/mappers/case-conversion-utils.ts new file mode 100644 index 0000000..74f8a9a --- /dev/null +++ b/src/services/api/mappers/case-conversion-utils.ts @@ -0,0 +1,100 @@ +import { camelCase, snakeCase } from '../utils/nativeUtils'; + +type TransformKeyCase< + S extends string, + Case extends 'camel' | 'snake' +> = S extends '_debug' + ? S // Exception: preserve '_debug' as-is for backwards compatibility + : Case extends 'camel' + ? S extends `_${infer RestAfterUnderscore}` + ? TransformKeyCase // Strip leading underscore for camelCase too + : S extends `${infer First}_${infer Rest}` + ? `${First}${Capitalize>}` + : S + : S extends `_${infer RestAfterUnderscore}` + ? TransformKeyCase // Strip leading underscore and process rest + : S extends `${infer Start}${infer Rest}` + ? `${Start extends Uppercase + ? '_' + : ''}${Lowercase}${TransformKeyCase}` + : S; + +export type DeepTransformKeysCase< + T, + Case extends 'camel' | 'snake', + IgnorePrivate extends boolean = false +> = T extends string + ? TransformKeyCase + : T extends object + ? { + [K in keyof T as K extends string + ? IgnorePrivate extends true + ? K extends `_${string}` + ? never // Exclude keys starting with underscore + : TransformKeyCase + : TransformKeyCase + : K]: DeepTransformKeysCase; + } + : T; + +/** + * Recursively transforms object keys using the provided transform function + * Handles nested objects, arrays, and preserves non-object values + */ +const transformKeysDeep = ( + obj: unknown, + transformFn: (key: string) => string, + ignorePrivate: boolean = false +): T => { + const _convertCasing = (val: unknown) => { + if (val === null || val === undefined) { + return obj; + } + + if (Array.isArray(val)) { + return val.map((item) => + transformKeysDeep(item, transformFn, ignorePrivate) + ); + } + + if (typeof val === 'object' && val.constructor === Object) { + const transformedObj: Record = {}; + + for (const [key, value] of Object.entries(val)) { + // Skips debug for backwards compatibility + if (key == '_debug') { + transformedObj[key] = value; + continue; + } + + const transformedKey = transformFn(key); + transformedObj[transformedKey] = transformKeysDeep( + value, + transformFn, + ignorePrivate + ); + } + + return transformedObj; + } + + // Return primitive values unchanged + return val; + }; + + return _convertCasing(obj) as T; +}; + +/** + * Converts object keys from camelCase to snake_case recursively + */ +export const convertToSnakeCase = ( + obj: T +): DeepTransformKeysCase => transformKeysDeep(obj, snakeCase); + +/** + * Converts object keys from snake_case to camelCase recursively + */ +export const convertToCamelCase = ( + obj: T +): DeepTransformKeysCase => transformKeysDeep(obj, camelCase); diff --git a/src/services/api/mappers/sessions/toSessionResponse.ts b/src/services/api/mappers/sessions/toSessionResponse.ts new file mode 100644 index 0000000..4fbf1d0 --- /dev/null +++ b/src/services/api/mappers/sessions/toSessionResponse.ts @@ -0,0 +1,18 @@ +import { CreateSessionResponse } from '../../../../types'; +import { + convertToCamelCase, + DeepTransformKeysCase, +} from '../case-conversion-utils'; +import { isSessionResponse } from './type-guards'; + +export const toSessionResponse = ( + response: DeepTransformKeysCase +) => { + const _res = convertToCamelCase(response); + + if (isSessionResponse(_res)) { + return _res; + } + + return undefined; +}; diff --git a/src/services/api/mappers/sessions/type-guards.ts b/src/services/api/mappers/sessions/type-guards.ts new file mode 100644 index 0000000..6886dba --- /dev/null +++ b/src/services/api/mappers/sessions/type-guards.ts @@ -0,0 +1,7 @@ +import { isNil } from '../../utils/nativeUtils'; +import { CreateSessionResponse } from '../../../../types'; + +export const isSessionResponse = (val: unknown): val is CreateSessionResponse => + !isNil((val as CreateSessionResponse)?.expiresAt) && + !isNil((val as CreateSessionResponse)?.nonce) && + !isNil((val as CreateSessionResponse)?.sessionKey); diff --git a/src/services/api/mappers/token-intents/toTokenIntentRequest.ts b/src/services/api/mappers/token-intents/toTokenIntentRequest.ts new file mode 100644 index 0000000..46c161a --- /dev/null +++ b/src/services/api/mappers/token-intents/toTokenIntentRequest.ts @@ -0,0 +1,15 @@ +import { CreateTokenIntent } from '../../../../types'; +import { convertToSnakeCase } from '../case-conversion-utils'; +import { isTokenIntentRequest } from './type-guards'; + +export const toTokenIntentRequest = (payload: Partial) => { + const _payload = convertToSnakeCase(payload); + + return isTokenIntentRequest(_payload) + ? { + ..._payload, + data: payload.data, + // metadata: payload.metadata Token Intents do not support metadata + } + : undefined; +}; diff --git a/src/services/api/mappers/token-intents/toTokenIntentResponse.ts b/src/services/api/mappers/token-intents/toTokenIntentResponse.ts new file mode 100644 index 0000000..272efe8 --- /dev/null +++ b/src/services/api/mappers/token-intents/toTokenIntentResponse.ts @@ -0,0 +1,21 @@ +import { TokenIntent } from '../../../../types'; +import { + convertToCamelCase, + DeepTransformKeysCase, +} from '../case-conversion-utils'; +import { isTokenIntentResponse } from './type-guards'; + +export const toTokenIntentResponse = ( + response: DeepTransformKeysCase +) => { + const _res = convertToCamelCase(response); + + if (isTokenIntentResponse(_res)) { + return { + ..._res, + data: response.data, + }; + } + + return undefined; +}; diff --git a/src/services/api/mappers/token-intents/type-guards.ts b/src/services/api/mappers/token-intents/type-guards.ts new file mode 100644 index 0000000..5f28f80 --- /dev/null +++ b/src/services/api/mappers/token-intents/type-guards.ts @@ -0,0 +1,10 @@ +import { isNil } from '../../utils/nativeUtils'; +import { TokenIntent } from '../../../../types'; + +export const isTokenIntentRequest = (val: unknown): val is TokenIntent => + !isNil((val as TokenIntent)?.type); + +export const isTokenIntentResponse = (val: unknown): val is TokenIntent => + !isNil((val as TokenIntent)?.id) && + !isNil((val as TokenIntent)?.type) && + !isNil((val as TokenIntent)?.tenantId); diff --git a/src/services/api/mappers/tokens/toTokenRequest.ts b/src/services/api/mappers/tokens/toTokenRequest.ts new file mode 100644 index 0000000..75250d8 --- /dev/null +++ b/src/services/api/mappers/tokens/toTokenRequest.ts @@ -0,0 +1,11 @@ +import { CreateToken } from '../../../../types'; +import { convertToSnakeCase } from '../case-conversion-utils'; +import { isTokenRequest } from './type-guards'; + +export const toTokenRequest = (payload: Partial): Record | undefined => { + const _payload = convertToSnakeCase(payload); + + return isTokenRequest(_payload) + ? { ..._payload, data: payload.data, metadata: payload.metadata } + : undefined; +}; diff --git a/src/services/api/mappers/tokens/toTokenResponse.ts b/src/services/api/mappers/tokens/toTokenResponse.ts new file mode 100644 index 0000000..3e976dd --- /dev/null +++ b/src/services/api/mappers/tokens/toTokenResponse.ts @@ -0,0 +1,34 @@ +import { Token } from '../../../../types'; +import { convertToCamelCase } from '../case-conversion-utils'; +import { isPaginatedList, isTokenResponse, TokenResponse } from './type-guards'; + +export const toPaginatedTokensResponse = (response: TokenResponse): { data: Token[]; pagination: Record } | undefined => { + if (isPaginatedList(response)) { + const transformedData = response.data.filter(isTokenResponse).map((t) => ({ + ...convertToCamelCase(t), + data: t.data, + metadata: t.metadata, + } as Token)); + + return { + data: transformedData, + pagination: convertToCamelCase(response.pagination), + }; + } + + return undefined; +}; + +export const toTokenResponse = (response: TokenResponse): Token | undefined => { + const _res = convertToCamelCase(response); + + if (isTokenResponse(_res)) { + return { + ..._res, + data: response.data, + metadata: response.metadata, + }; + } + + return undefined; +}; diff --git a/src/services/api/mappers/tokens/toUpdateTokenRequest.ts b/src/services/api/mappers/tokens/toUpdateTokenRequest.ts new file mode 100644 index 0000000..99c3f57 --- /dev/null +++ b/src/services/api/mappers/tokens/toUpdateTokenRequest.ts @@ -0,0 +1,11 @@ +import { UpdateToken } from '../../../../types'; +import { convertToSnakeCase } from '../case-conversion-utils'; +import { isUpdateToken } from './type-guards'; + +export const toUpdateTokenRequest = (payload: Partial) => { + const _payload = convertToSnakeCase(payload); + + return isUpdateToken(_payload) + ? { ..._payload, data: payload.data, metadata: payload.metadata } + : undefined; +}; diff --git a/src/services/api/mappers/tokens/type-guards.ts b/src/services/api/mappers/tokens/type-guards.ts new file mode 100644 index 0000000..714348c --- /dev/null +++ b/src/services/api/mappers/tokens/type-guards.ts @@ -0,0 +1,35 @@ +import { isNil } from '../../utils/nativeUtils'; +import { Token, UpdateToken } from '../../../../types'; +import { DeepTransformKeysCase } from '../case-conversion-utils'; + +export const isTokenRequest = (val: unknown): val is Token => + !isNil((val as Token)?.data) && !isNil((val as Token)?.type); + +// TODO: extend this with all the updateable props +export const isUpdateToken = (val: unknown): val is UpdateToken => + !isNil((val as UpdateToken)?.data) || + !isNil((val as UpdateToken)?.metadata) || + !isNil((val as UpdateToken)?.expiresAt); + +interface PaginatedList { + pagination: { + totalItems?: number; + pageNumber?: number; + pageSize: number; + totalPages?: number; + after?: string; + }; + data: T[]; +} + +export const isTokenResponse = (val: unknown): val is Token => + // data || type to account for `redact` transform in access rules + (!isNil((val as Token)?.data) || !isNil((val as Token)?.type)) && + !isNil((val as Token)?.tenantId); + +export const isPaginatedList = (arg: unknown): arg is PaginatedList => + (arg as PaginatedList) && + (arg as PaginatedList)?.pagination !== undefined && + (arg as PaginatedList)?.data !== undefined; + +export type TokenResponse = DeepTransformKeysCase; diff --git a/src/services/api/proxy-client.ts b/src/services/api/proxy-client.ts new file mode 100644 index 0000000..5f62e4c --- /dev/null +++ b/src/services/api/proxy-client.ts @@ -0,0 +1,279 @@ +import { + BasisTheoryApiError, + BasisTheoryValidationError, + ProxyRequestOptions, +} from '../../types'; +import { getDeviceInfo } from '../deviceInfo'; + +const API_KEY_HEADER = 'BT-API-KEY'; +const BT_TRACE_ID_HEADER = 'bt-trace-id'; +const BT_IDEMPOTENCY_KEY_HEADER = 'bt-idempotency-key'; +const BT_PROXY_URL_HEADER = 'BT-PROXY-URL'; +const BT_DEVICE_INFO_HEADER = 'BT-DEVICE-INFO'; +const CF_RAY = 'cf-ray'; + +interface ProxyConfig { + apiKey?: string; + baseUrl: string; + debug?: boolean; +} + +type ProxyMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'; + +const buildProxyHeaders = ( + config: ProxyConfig, + options?: ProxyRequestOptions +): Headers => { + const headers = new Headers(); + + // Set content type for requests with bodies + headers.set('Content-Type', 'application/json'); + + // Set API key + if (options?.apiKey || config.apiKey) { + headers.set(API_KEY_HEADER, options?.apiKey || config.apiKey!); + } + + // Set correlation ID for tracing + if (options?.correlationId) { + headers.set(BT_TRACE_ID_HEADER, options.correlationId); + } + + // Set idempotency key + if (options?.idempotencyKey) { + headers.set(BT_IDEMPOTENCY_KEY_HEADER, options.idempotencyKey); + } + + // Set custom headers from options + if (options?.headers) { + Object.entries(options?.headers).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + headers.set(key, String(value)); + } + }); + } + + // Automatically collect and include device info + // Use provided deviceInfo or collect it automatically + const deviceInfo = options?.deviceInfo ?? getDeviceInfo(); + const data = JSON.stringify(deviceInfo); + // Use btoa for base64 encoding - available in React Native via polyfill + const base64 = typeof btoa !== 'undefined' + ? btoa(data) + : Buffer.from(data).toString('base64'); + headers.set(BT_DEVICE_INFO_HEADER, base64); + + return headers; +}; + +const buildProxyUrl = ( + baseUrl: string, + path?: string, + query?: Record +): string => { + // Input validation + if (!baseUrl || typeof baseUrl !== 'string') { + throw new TypeError('baseUrl must be a non-empty string'); + } + + let url = baseUrl; + + // Explicit path handling with clear semantics + if (path === undefined) { + // No path modification - use baseUrl exactly as provided + url = baseUrl; + } else if (path === '') { + // Empty string explicitly requests trailing slash + url = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + } else { + // Non-empty path: normalize to prevent double slashes + const normalizedBase = baseUrl.endsWith('/') + ? baseUrl.slice(0, -1) + : baseUrl; + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + url = `${normalizedBase}${normalizedPath}`; + } + + // Add query parameters if provided + if (query && Object.keys(query).length > 0) { + const searchParams = new URLSearchParams(); + + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + + url = `${url}?${searchParams.toString()}`; + } + + return url; +}; + +const makeProxyRequest = async ( + config: ProxyConfig, + method: ProxyMethods, + options?: ProxyRequestOptions +): Promise<{ data: unknown; headers: Record }> => { + const headers = buildProxyHeaders(config, options); + + const proxyUrl = + options?.headers?.['BT-PROXY-URL'] || options?.headers?.['bt-proxy-url']; + + const proxyKey = + options?.headers?.['BT-PROXY-KEY'] || options?.headers?.['bt-proxy-key']; + + if (proxyUrl && proxyKey) { + throw new BasisTheoryValidationError( + 'Only one of BT-PROXY-URL or BT-PROXY-KEY is required for proxy requests', + {}, + [] + ); + } + + if (!proxyUrl && !proxyKey) { + throw new BasisTheoryValidationError( + 'Either BT-PROXY-URL or BT-PROXY-KEY header is required for proxy requests', + {}, + [] + ); + } + + const destinationUrl = buildProxyUrl( + String(proxyUrl), + options?.path, + options?.query + ); + + headers.set(BT_PROXY_URL_HEADER, destinationUrl); + + const requestUrl = `${config.baseUrl}/proxy`; + + const requestInit: RequestInit = { + method: method.toUpperCase(), + headers, + }; + + if ( + options?.body && + (method === 'post' || method === 'put' || method === 'patch') + ) { + requestInit.body = JSON.stringify(options.body); + } + + const response = await fetch(requestUrl, requestInit); + + let data: unknown; + const contentType = response.headers.get('content-type'); + + try { + if (contentType?.includes('application/json')) { + data = await response.json(); + } else { + data = await response.text(); + } + } catch { + data = null; + } + + if (!response.ok) { + if ( + response.status >= 400 && + response.status < 500 && + typeof data === 'object' && + data && + 'errors' in data + ) { + const errorData = data as { title?: string; errors: unknown }; + throw new BasisTheoryValidationError( + errorData.title || 'Validation failed', + errorData.errors, + [] // deprecated validation field + ); + } + + const errorData = data as { title?: string }; + throw new BasisTheoryApiError( + typeof data === 'object' && data && 'title' in data + ? errorData.title || 'Proxy request failed' + : 'Proxy request failed', + response.status, + data + ); + } + + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + const cfRay = response.headers.get(CF_RAY); + const btTraceId = response.headers.get(BT_TRACE_ID_HEADER); + const debugInfo = config.debug ? { cfRay, btTraceId } : undefined; + + if (options?.includeResponseHeaders) { + return { + data, + headers: responseHeaders, + ...{ _debug: debugInfo }, + }; + } + + return { + data: + typeof data === 'string' + ? data + : { + ...(typeof data === 'object' && data ? data : { data }), + ...{ _debug: debugInfo }, + }, + headers: responseHeaders, + }; +}; + +// Proxy service methods +const proxyGet = + (config: ProxyConfig) => + ( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }> => + makeProxyRequest(config, 'get', options); + +const proxyPost = + (config: ProxyConfig) => + ( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }> => + makeProxyRequest(config, 'post', options); + +const proxyPut = + (config: ProxyConfig) => + ( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }> => + makeProxyRequest(config, 'put', options); + +const proxyPatch = + (config: ProxyConfig) => + ( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }> => + makeProxyRequest(config, 'patch', options); + +const proxyDelete = + (config: ProxyConfig) => + ( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }> => + makeProxyRequest(config, 'delete', options); + +// Create proxy service functions bound to config +const createProxyClient = (config: ProxyConfig) => ({ + get: proxyGet(config), + post: proxyPost(config), + put: proxyPut(config), + patch: proxyPatch(config), + delete: proxyDelete(config), +}); + +export { createProxyClient, type ProxyConfig }; diff --git a/src/services/api/utils/nativeUtils.ts b/src/services/api/utils/nativeUtils.ts new file mode 100644 index 0000000..6400157 --- /dev/null +++ b/src/services/api/utils/nativeUtils.ts @@ -0,0 +1,117 @@ +/** + * Native JavaScript utility functions + * Simplified versions for React Native Elements + */ + +// Replace ramda/isNil +export const isNil = (value: unknown): value is null | undefined => { + return value == null; +}; + +/** + * Converts string to string representation for safe processing + */ +const toString = (value: unknown): string => { + if (value == null) return ''; + if (typeof value === 'string') return value; + return String(value); +}; + +/** + * Capitalizes the first character of a string + */ +const capitalize = (string: string): string => { + if (!string) return string; + return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +}; + +/** + * Checks if string contains Unicode word characters + */ +const hasUnicodeWord = (string: string): boolean => { + return /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/.test( + string + ); +}; + +/** + * Extracts ASCII words from string + */ +const asciiWords = (string: string): string[] => { + const words = string.split(/[^a-zA-Z0-9]+/).filter((word) => word.length > 0); + return words; +}; + +/** + * Extracts Unicode words from string + */ +const unicodeWords = (string: string): string[] => { + const pattern = [ + '[A-Z][a-z]+', + '[A-Z]+(?=[A-Z][a-z]|[^a-zA-Z]|$)', + '[a-z]+', + '\\d+', + '[\\u00C0-\\u024F\\u1E00-\\u1EFF]+', + ].join('|'); + + const matches = string.match(new RegExp(pattern, 'g')); + return matches || []; +}; + +/** + * Extracts words from string, handling both ASCII and Unicode + */ +const words = (string: string): string[] => { + const str = toString(string); + if (!str) return []; + + return hasUnicodeWord(str) ? unicodeWords(str) : asciiWords(str); +}; + +/** + * Reduces array using callback function + */ +const arrayReduce = ( + array: T[], + callback: (accumulator: R, value: T, index: number) => R, + initialValue: R +): R => { + return array.reduce(callback, initialValue); +}; + +/** + * Creates a compounder function + */ +const createCompounder = ( + callback: (result: string, word: string, index: number) => string +) => { + return (string: unknown): string => { + const str = toString(string); + if (!str) return ''; + + // Remove apostrophes + const cleaned = str.replace(/['\u2019]/g, ''); + const wordsArray = words(cleaned); + + return arrayReduce(wordsArray, callback, ''); + }; +}; + +/** + * Converts string to snake_case + */ +export const snakeCase = createCompounder( + (result: string, word: string, index: number): string => { + return result + (index ? '_' : '') + word.toLowerCase(); + } +); + +/** + * Converts string to camelCase + */ +export const camelCase = createCompounder( + (result: string, word: string, index: number): string => { + const processedWord = word.toLowerCase(); + return result + (index ? capitalize(processedWord) : processedWord); + } +); diff --git a/src/services/basis-theory-js.ts b/src/services/basis-theory-js.ts new file mode 100644 index 0000000..49393c5 --- /dev/null +++ b/src/services/basis-theory-js.ts @@ -0,0 +1,106 @@ +import { createBasisTheoryApi, type BasisTheoryConfig } from './api/bt-client'; +import { createProxyClient } from './api/proxy-client'; + +let basisTheoryApi: ReturnType; +let proxyClient: ReturnType; +let basisTheoryConfig: BasisTheoryConfig; + +// API URL Constants +const API_URLS = { + LOCALHOST: 'http://localhost:3333', + UAT: 'https://api.btsandbox.com', + + DEV: { + STANDARD: 'https://api.flock-dev.com', + NG: 'https://api-ng.flock-dev.com', + }, + + PROD: { + STANDARD: 'https://api.basistheory.com', + NG: 'https://api-ng.basistheory.com', + }, +} as const; + +/** + * Determines if current environment is development + */ +const isDevEnvironment = (apiBaseUrl?: string): boolean => + Boolean(apiBaseUrl?.includes('flock-dev')); + +const getDefaultApiBaseUrl = ( + apiBaseUrl?: string, + useNgApi?: boolean, + useUat?: boolean +): string => { + // If custom URL provided, use it + if (apiBaseUrl) { + return apiBaseUrl; + } + + // UAT environment + if (useUat) { + return API_URLS.UAT; + } + + // Development environment + if (isDevEnvironment(apiBaseUrl)) { + return useNgApi ? API_URLS.DEV.NG : API_URLS.DEV.STANDARD; + } + + // Production environment (default) + return useNgApi ? API_URLS.PROD.NG : API_URLS.PROD.STANDARD; +}; + +const loadBasisTheoryInstance = async ( + apiKey?: string, + apiBaseUrl?: string, + useNgApi?: boolean, + debug?: boolean, + useUat?: boolean +): Promise => { + if (basisTheoryApi && proxyClient) { + return; + } + + const baseUrl = getDefaultApiBaseUrl(apiBaseUrl, useNgApi, useUat); + + basisTheoryConfig = { + apiKey, + baseUrl, + debug, + }; + + basisTheoryApi = createBasisTheoryApi(basisTheoryConfig); + proxyClient = createProxyClient(basisTheoryConfig); +}; + +// Create a proxy interface that matches the old BasisTheory SDK +const getBasisTheoryInstance = () => { + if (!basisTheoryApi || !proxyClient) { + throw new Error( + 'BasisTheory instance not initialized. Call loadBasisTheoryInstance first.' + ); + } + + return { + ...basisTheoryApi, + // Use the new proxy client + proxy: proxyClient, + }; +}; + +const getBasisTheoryConfig = (): BasisTheoryConfig => { + if (!basisTheoryConfig) { + throw new Error( + 'BasisTheory config not initialized. Call loadBasisTheoryInstance first.' + ); + } + return basisTheoryConfig; +}; + +export { + getBasisTheoryConfig, + getBasisTheoryInstance, + getDefaultApiBaseUrl, + loadBasisTheoryInstance, +}; diff --git a/src/services/deviceInfo.ts b/src/services/deviceInfo.ts new file mode 100644 index 0000000..49a749e --- /dev/null +++ b/src/services/deviceInfo.ts @@ -0,0 +1,39 @@ +import { Platform, Dimensions } from 'react-native'; +import type { DeviceInfo } from '../types'; + +/** + * Collects device information for React Native applications + * This information is sent with API requests for analytics and security purposes + */ +export const getDeviceInfo = (): DeviceInfo => { + const { width, height } = Dimensions.get('window'); + const screenDimensions = Dimensions.get('screen'); + + const deviceInfo: DeviceInfo = { + // Platform information + platform: Platform.OS, + uaPlatform: Platform.OS, + + // Screen dimensions + screenWidth: screenDimensions.width, + screenHeight: screenDimensions.height, + innerWidth: width, + innerHeight: height, + + // Device capabilities + uaMobile: Platform.OS === 'ios' || Platform.OS === 'android', + }; + + // Add platform version if available + if (Platform.Version) { + deviceInfo.uaPlatformVersion = String(Platform.Version); + } + + // Add pixel ratio if available + const pixelRatio = Dimensions.get('window').scale; + if (pixelRatio) { + deviceInfo.devicePixelRatio = pixelRatio; + } + + return deviceInfo; +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..bbe565a --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,3 @@ +export * from './models'; +export * from './services/shared'; +export * from './services/basis-theory'; diff --git a/src/types/models/card-types.ts b/src/types/models/card-types.ts new file mode 100644 index 0000000..c154996 --- /dev/null +++ b/src/types/models/card-types.ts @@ -0,0 +1,216 @@ +export interface CreditCardType { + niceType: string; + type: string; + patterns: (number | number[])[]; + gaps: number[]; + lengths: number[]; + code: { + name: string; + size: number; + }; +} + +// Common card type constants +export const VISA: CreditCardType = { + niceType: 'Visa', + type: 'visa', + patterns: [4], + gaps: [4, 8, 12], + lengths: [16, 18, 19], + code: { + name: 'CVV', + size: 3, + }, +}; + +export const MASTERCARD: CreditCardType = { + niceType: 'Mastercard', + type: 'mastercard', + patterns: [[51, 55], [2221, 2229], [223, 229], [23, 26], [270, 271], 2720], + gaps: [4, 8, 12], + lengths: [16], + code: { + name: 'CVC', + size: 3, + }, +}; + +export const AMERICAN_EXPRESS: CreditCardType = { + niceType: 'American Express', + type: 'american-express', + patterns: [34, 37], + gaps: [4, 10], + lengths: [15], + code: { + name: 'CID', + size: 4, + }, +}; + +export const DINERS_CLUB: CreditCardType = { + niceType: 'Diners Club', + type: 'diners-club', + patterns: [[300, 305], 36, 38, 39], + gaps: [4, 10], + lengths: [14, 16, 19], + code: { + name: 'CVV', + size: 3, + }, +}; + +export const DISCOVER: CreditCardType = { + niceType: 'Discover', + type: 'discover', + patterns: [6011, [644, 649], 65], + gaps: [4, 8, 12], + lengths: [16, 19], + code: { + name: 'CID', + size: 3, + }, +}; + +export const JCB: CreditCardType = { + niceType: 'JCB', + type: 'jcb', + patterns: [2131, 1800, [3528, 3589]], + gaps: [4, 8, 12], + lengths: [16, 17, 18, 19], + code: { + name: 'CVV', + size: 3, + }, +}; + +export const UNIONPAY: CreditCardType = { + niceType: 'UnionPay', + type: 'unionpay', + patterns: [ + 620, + [624, 626], + [62100, 62182], + [62184, 62187], + [62185, 62197], + [62200, 62205], + [622010, 622999], + 622018, + [622019, 622999], + [62207, 62209], + [622126, 622925], + [623, 626], + 6270, + 6272, + 6276, + [627700, 627779], + [627781, 627799], + [6282, 6289], + 6291, + 6292, + 810, + [8110, 8131], + [8132, 8151], + [8152, 8163], + [8164, 8171], + ], + gaps: [4, 8, 12], + lengths: [14, 15, 16, 17, 18, 19], + code: { + name: 'CVN', + size: 3, + }, +}; + +export const MAESTRO: CreditCardType = { + niceType: 'Maestro', + type: 'maestro', + patterns: [ + 493698, + [500000, 506698], + [506779, 508999], + [56, 59], + 63, + 67, + 6, + ], + gaps: [4, 8, 12], + lengths: [12, 13, 14, 15, 16, 17, 18, 19], + code: { + name: 'CVC', + size: 3, + }, +}; + +export const ELO: CreditCardType = { + niceType: 'Elo', + type: 'elo', + patterns: [ + 401178, + 401179, + 438935, + 457631, + 457632, + 431274, + 451416, + 457393, + 504175, + [506699, 506778], + [509000, 509999], + 627780, + 636297, + 636368, + [650031, 650033], + [650035, 650051], + [650405, 650439], + [650485, 650538], + [650541, 650598], + [650700, 650718], + [650720, 650727], + [650901, 650978], + [651652, 651679], + [655000, 655019], + [655021, 655058], + ], + gaps: [4, 8, 12], + lengths: [16], + code: { + name: 'CVE', + size: 3, + }, +}; + +export const MIR: CreditCardType = { + niceType: 'Mir', + type: 'mir', + patterns: [[2200, 2204]], + gaps: [4, 8, 12], + lengths: [16, 17, 18, 19], + code: { + name: 'CVP2', + size: 3, + }, +}; + +export const HIPER: CreditCardType = { + niceType: 'Hiper', + type: 'hiper', + patterns: [637095, 63737423, 63743358, 637568, 637599, 637609, 637612], + gaps: [4, 8, 12], + lengths: [16], + code: { + name: 'CVC', + size: 3, + }, +}; + +export const HIPERCARD: CreditCardType = { + niceType: 'Hipercard', + type: 'hipercard', + patterns: [606282], + gaps: [4, 8, 12], + lengths: [16], + code: { + name: 'CVC', + size: 3, + }, +}; diff --git a/src/types/models/device.ts b/src/types/models/device.ts new file mode 100644 index 0000000..bfdd042 --- /dev/null +++ b/src/types/models/device.ts @@ -0,0 +1,47 @@ +interface NetworkInformation { + downlink: number; + downlinkMax?: number; + effectiveType: 'slow-2g' | '2g' | '3g' | '4g'; + rtt: number; + saveData: boolean; + type?: + | 'bluetooth' + | 'cellular' + | 'ethernet' + | 'none' + | 'wifi' + | 'wimax' + | 'other' + | 'unknown'; +} + +export type DeviceInfo = { + uaBrands?: Array<{ brand: string; version: string }>; + uaMobile?: boolean; + uaPlatform?: string; + uaPlatformVersion?: string; + languages?: string[]; + timeZone?: string; + cookiesEnabled?: boolean; + localStorageEnabled?: boolean; + sessionStorageEnabled?: boolean; + platform?: string; + hardwareConcurrency?: number; + deviceMemoryGb?: number | null; + screenWidth?: number; + screenHeight?: number; + screenAvailWidth?: number; + screenAvailHeight?: number; + innerWidth?: number; + innerHeight?: number; + devicePixelRatio?: number; + maxTouchPoints?: number; + plugins?: string[]; + mimeTypes?: string[]; + webdriver?: boolean; + suspectedHeadless?: boolean; + webglVendor?: string; + webglRenderer?: string; + canvasHash?: string; + network?: NetworkInformation; +}; diff --git a/src/types/models/errors.ts b/src/types/models/errors.ts new file mode 100644 index 0000000..2a01f92 --- /dev/null +++ b/src/types/models/errors.ts @@ -0,0 +1,23 @@ +export class BasisTheoryApiError extends Error { + public readonly status: number; + public readonly data?: unknown; + + public constructor(message: string, status: number, data?: unknown) { + super(message); + this.name = 'BasisTheoryApiError'; + this.status = status; + this.data = data; + } +} + +export class BasisTheoryValidationError extends Error { + public readonly errors: unknown; + public readonly validation: unknown[]; + + public constructor(message: string, errors: unknown, validation: unknown[]) { + super(message); + this.name = 'BasisTheoryValidationError'; + this.errors = errors; + this.validation = validation; + } +} diff --git a/src/types/models/index.ts b/src/types/models/index.ts new file mode 100644 index 0000000..62fc810 --- /dev/null +++ b/src/types/models/index.ts @@ -0,0 +1,9 @@ +export * from './shared'; +export * from './tokens'; +export * from './tokenize'; +export * from './sessions'; +export * from './token-intents'; +export * from './errors'; +export * from './device'; +export * from './proxy'; +export * from './card-types'; diff --git a/src/types/models/proxy.ts b/src/types/models/proxy.ts new file mode 100644 index 0000000..bb3e18c --- /dev/null +++ b/src/types/models/proxy.ts @@ -0,0 +1,13 @@ +import { DeviceInfo } from './device'; + +export interface ProxyRequestOptions { + headers?: Record; + apiKey?: string; + path?: string; + query?: Record; + body?: unknown; + correlationId?: string; + idempotencyKey?: string; + includeResponseHeaders?: boolean; + deviceInfo?: DeviceInfo; +} diff --git a/src/types/models/sessions.ts b/src/types/models/sessions.ts new file mode 100644 index 0000000..8ab798f --- /dev/null +++ b/src/types/models/sessions.ts @@ -0,0 +1,9 @@ +export interface CreateSessionResponse { + nonce: string; + expiresAt: string; + sessionKey: string; + _debug?: { + cfRay?: string; + btTraceId?: string; + }; +} diff --git a/src/types/models/shared.ts b/src/types/models/shared.ts new file mode 100644 index 0000000..828b61c --- /dev/null +++ b/src/types/models/shared.ts @@ -0,0 +1,9 @@ +export interface Privacy { + classification?: string; + impactLevel?: string; + restrictionPolicy?: string; +} + +export interface Metadata { + [key: string]: string; +} diff --git a/src/types/models/token-intents.ts b/src/types/models/token-intents.ts new file mode 100644 index 0000000..7553d4e --- /dev/null +++ b/src/types/models/token-intents.ts @@ -0,0 +1,19 @@ +export interface TokenIntent { + id: string; + type: string; + tenantId: string; + data?: T; + createdBy?: string; + createdAt?: string; + expiresAt?: string; + _debug?: { + cfRay?: string; + btTraceId?: string; + }; +} + +export interface CreateTokenIntent { + type: string; + data: T; + expiresAt?: string; +} diff --git a/src/types/models/tokenize.ts b/src/types/models/tokenize.ts new file mode 100644 index 0000000..3766dca --- /dev/null +++ b/src/types/models/tokenize.ts @@ -0,0 +1,3 @@ +export type TokenizeData = T; + +export type TokenizeDataModel = T; diff --git a/src/types/models/tokens.ts b/src/types/models/tokens.ts new file mode 100644 index 0000000..c4d5368 --- /dev/null +++ b/src/types/models/tokens.ts @@ -0,0 +1,53 @@ +import { Metadata, Privacy } from './shared'; + +export interface Token { + id: string; + type: string; + tenantId: string; + data?: T; + createdBy?: string; + createdAt?: string; + modifiedBy?: string; + modifiedAt?: string; + fingerprint?: string; + fingerprintExpression?: string; + mask?: unknown; + privacy?: Privacy; + searchIndexes?: string[]; + metadata?: Metadata; + expiresAt?: string; + containers?: string[]; + aliasId?: string; + _debug?: { + cfRay?: string; + btTraceId?: string; + }; + [key: string]: unknown; +} + +export interface CreateToken { + id?: string; + type: string; + data: T; + fingerprint?: string; + fingerprintExpression?: string; + mask?: unknown; + privacy?: Privacy; + searchIndexes?: string[]; + metadata?: Metadata; + expiresAt?: string; + deduplicateToken?: boolean; + containers?: string[]; +} + +export interface UpdateToken { + data?: T; + privacy?: Privacy; + searchIndexes?: string[]; + metadata?: Metadata; + expiresAt?: string | null; + deduplicateToken?: boolean; + containers?: string[]; + mask?: unknown; + fingerprintExpression?: string; +} diff --git a/src/types/services/basis-theory.ts b/src/types/services/basis-theory.ts new file mode 100644 index 0000000..33498a0 --- /dev/null +++ b/src/types/services/basis-theory.ts @@ -0,0 +1,64 @@ +import type { + CreateToken, + UpdateToken, + Token, + TokenizeData, + TokenizeDataModel, + CreateSessionResponse, + CreateTokenIntent, + TokenIntent, + ProxyRequestOptions, +} from '../models'; +import type { RequestOptions } from './shared'; + +export interface BasisTheoryTokensService { + create(payload: CreateToken, options?: RequestOptions): Promise; + update( + id: string, + payload: UpdateToken, + options?: RequestOptions + ): Promise; + retrieve(id: string, options?: RequestOptions): Promise; + delete(id: string, options?: RequestOptions): Promise; +} + +export interface BasisTheorySessionsService { + create(payload?: unknown, options?: RequestOptions): Promise; +} + +export interface BasisTheoryTokenIntentsService { + create( + payload: CreateTokenIntent, + options?: RequestOptions + ): Promise; + delete(id: string, options?: RequestOptions): Promise; +} + +export interface BasisTheoryProxyService { + get( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }>; + post( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }>; + put( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }>; + patch( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }>; + delete( + options?: ProxyRequestOptions + ): Promise<{ data: unknown; headers: Record }>; +} + +export interface BasisTheoryInstance { + tokens: BasisTheoryTokensService; + tokenize( + payload: TokenizeData, + options?: RequestOptions + ): Promise; + sessions: BasisTheorySessionsService; + tokenIntents: BasisTheoryTokenIntentsService; + proxy: BasisTheoryProxyService; +} diff --git a/src/types/services/shared.ts b/src/types/services/shared.ts new file mode 100644 index 0000000..dd81a60 --- /dev/null +++ b/src/types/services/shared.ts @@ -0,0 +1,8 @@ +import { DeviceInfo } from '../models/device'; + +export interface RequestOptions { + apiKey?: string; + correlationId?: string; + idempotencyKey?: string; + deviceInfo?: DeviceInfo; +} diff --git a/src/useBasisTheory.ts b/src/useBasisTheory.ts index 5dba178..8cfc1df 100644 --- a/src/useBasisTheory.ts +++ b/src/useBasisTheory.ts @@ -1,8 +1,3 @@ -import { BasisTheory } from '@basis-theory/basis-theory-js'; -import type { - BasisTheoryInitOptionsWithoutElements, - BasisTheory as BasisTheoryType, -} from '@basis-theory/basis-theory-js/types/sdk'; import { useEffect, useState } from 'react'; import { @@ -13,15 +8,26 @@ import { Proxy } from './modules/proxy'; import { Sessions } from './modules/sessions'; import { TokenIntents } from './modules/tokenIntents'; import { Tokens } from './modules/tokens'; +import { loadBasisTheoryInstance, getBasisTheoryInstance } from './services/basis-theory-js'; +import type { BasisTheoryInstance } from './types'; + +interface BasisTheoryInitOptions { + apiBaseUrl?: string; + useNgApi?: boolean; + debug?: boolean; + useUat?: boolean; +} const _BasisTheoryElements = async ({ apiKey, apiBaseUrl, -}: BasisTheoryInitOptionsWithoutElements & { apiKey: string }) => { - const bt: BasisTheoryType = await new BasisTheory().init( - apiKey, - apiBaseUrl ? { apiBaseUrl } : undefined - ); + useNgApi, + debug, + useUat, +}: BasisTheoryInitOptions & { apiKey: string }) => { + await loadBasisTheoryInstance(apiKey, apiBaseUrl, useNgApi, debug, useUat); + + const bt: BasisTheoryInstance = getBasisTheoryInstance(); const { setConfig } = _useConfigManager(); @@ -52,7 +58,7 @@ type UseBasisTheory = { const useBasisTheory = ( apiKey: string, - options?: BasisTheoryInitOptionsWithoutElements + options?: BasisTheoryInitOptions ): UseBasisTheory => { const [state, setState] = useState({}); diff --git a/src/utils/shared.ts b/src/utils/shared.ts index f2dae5e..3f1735b 100644 --- a/src/utils/shared.ts +++ b/src/utils/shared.ts @@ -1,4 +1,4 @@ -import type { Token } from '@basis-theory/basis-theory-js/types/models'; +import type { Token } from '../types'; import { anyPass, equals, is, isEmpty, isNil, replace, type } from 'ramda'; import type { BTRef, InputBTRefWithDatepart } from '../BaseElementTypes'; import type { CardBrand } from '../CardElementTypes'; diff --git a/tests/components/CardNumberElement.test.tsx b/tests/components/CardNumberElement.test.tsx index 5464f2d..a66b998 100644 --- a/tests/components/CardNumberElement.test.tsx +++ b/tests/components/CardNumberElement.test.tsx @@ -2,7 +2,7 @@ * @format */ -import { VISA, MASTERCARD } from '@basis-theory/basis-theory-js/types/elements'; +import { VISA, MASTERCARD } from '../../src/types'; import 'react-native'; import React from 'react'; diff --git a/tests/modules/tokenEncryption.test.ts b/tests/modules/tokenEncryption.test.ts index 5ad07af..e7a38b9 100644 --- a/tests/modules/tokenEncryption.test.ts +++ b/tests/modules/tokenEncryption.test.ts @@ -3,7 +3,7 @@ import { CreateTokenWithBtRef, Tokens, } from '../../src/modules/tokens'; -import type { BasisTheory as BasisTheoryType } from '@basis-theory/basis-theory-js/types/sdk'; +import type { BasisTheoryInstance as BasisTheoryType } from '../../src/types'; import { EncryptValidationError } from '../../src/services/tokenEncryption'; jest.mock('../../src/ElementValues', () => ({ diff --git a/tests/modules/tokenIntents.test.ts b/tests/modules/tokenIntents.test.ts index 20673e3..6d18d8c 100644 --- a/tests/modules/tokenIntents.test.ts +++ b/tests/modules/tokenIntents.test.ts @@ -4,7 +4,7 @@ import { TokenIntents, TokenizeData, } from '../../src/modules/tokenIntents'; -import type { BasisTheory as BasisTheoryType } from '@basis-theory/basis-theory-js/types/sdk'; +import type { BasisTheoryInstance as BasisTheoryType } from '../../src/types'; jest.mock('../../src/ElementValues', () => ({ _elementValues: {}, diff --git a/tests/modules/tokens.test.ts b/tests/modules/tokens.test.ts index 64e6cb2..eddfe83 100644 --- a/tests/modules/tokens.test.ts +++ b/tests/modules/tokens.test.ts @@ -4,7 +4,7 @@ import { TokenizeData, Tokens, } from '../../src/modules/tokens'; -import type { BasisTheory as BasisTheoryType } from '@basis-theory/basis-theory-js/types/sdk'; +import type { BasisTheoryInstance as BasisTheoryType } from '../../src/types'; jest.mock('../../src/ElementValues', () => ({ _elementValues: {}, diff --git a/tests/utils/dataManipulationUtils.test.ts b/tests/utils/dataManipulationUtils.test.ts index fee11d4..b360be8 100644 --- a/tests/utils/dataManipulationUtils.test.ts +++ b/tests/utils/dataManipulationUtils.test.ts @@ -1,4 +1,4 @@ -import { Token } from '@basis-theory/basis-theory-js/types/models'; +import { Token } from '../../src/types'; import { BTRef } from '../../src'; import * as state from '../../src/ElementValues'; import { diff --git a/yarn.lock b/yarn.lock index 500b6bc..6491538 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1080,19 +1080,6 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@basis-theory/basis-theory-js@^4.28.2": - version "4.28.2" - resolved "https://registry.npmjs.org/@basis-theory/basis-theory-js/-/basis-theory-js-4.28.2.tgz" - integrity sha512-Gp4BZhIXgkx5o/cmK5+VHpFW90tTKtXekeeSbCua9pQ1bBovQEHk3by07A0aLj0jWA3gSefdcluVgIcUayTPoA== - dependencies: - axios "^1.8.2" - camelcase-keys "^6.2.2" - csstype "^3.0.11" - os "^0.1.2" - os-browserify "^0.3.0" - snake-case "^3.0.4" - snakecase-keys "^3.2.1" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -3172,7 +3159,7 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@1.10.0, axios@^1.8.2: +axios@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== @@ -3492,15 +3479,6 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== - dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" @@ -3953,7 +3931,7 @@ cssesc@^3.0.0: resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^3.0.11, csstype@^3.0.2: +csstype@^3.0.2: version "3.1.3" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -4134,14 +4112,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" @@ -6813,13 +6783,6 @@ loose-envify@^1.0.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - lru-cache@^10.0.1, lru-cache@^10.2.0, lru-cache@^10.2.2: version "10.4.3" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" @@ -6881,11 +6844,6 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -map-obj@^4.0.0, map-obj@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz" - integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== - marked-terminal@^7.0.0: version "7.2.1" resolved "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.2.1.tgz" @@ -7582,14 +7540,6 @@ nerf-dart@^1.0.0: resolved "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz" integrity sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g== -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - nocache@^3.0.1: version "3.0.4" resolved "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz" @@ -8011,16 +7961,6 @@ ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - -os@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/os/-/os-0.1.2.tgz" - integrity sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ== - p-each-series@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz" @@ -8522,11 +8462,6 @@ queue@6.0.2: dependencies: inherits "~2.0.3" -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - ramda@^0.30.1: version "0.30.1" resolved "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz" @@ -9253,22 +9188,6 @@ smart-buffer@^4.2.0: resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -snake-case@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -snakecase-keys@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.2.1.tgz" - integrity sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA== - dependencies: - map-obj "^4.1.0" - to-snake-case "^1.0.0" - socks-proxy-agent@^8.0.3: version "8.0.4" resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz" @@ -9767,11 +9686,6 @@ tmpl@1.0.5: resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== -to-no-case@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz" - integrity sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -9779,20 +9693,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -to-snake-case@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz" - integrity sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ== - dependencies: - to-space-case "^1.0.0" - -to-space-case@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz" - integrity sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA== - dependencies: - to-no-case "^1.0.0" - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" @@ -9823,7 +9723,7 @@ tslib@^1.8.1: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.6.2: +tslib@^2.6.2: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==