Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo/Collect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const Collect = () => {
const [tokenizedData, setTokenizedData] = useState<
TokenizeData | undefined
>();
const [encryptedToken, setEncryptedToken] = useState<EncryptedToken[] | undefined>();
const [encryptedToken, setEncryptedToken] = useState<EncryptedToken | undefined>();

const [tokenId, setTokenId] = useState('');

Expand Down
4 changes: 3 additions & 1 deletion src/model/EncryptTokenData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ type EncryptToken = {
* Result of token encryption operation.
* Contains the encrypted token string and its original type.
*/
type EncryptedToken = {
type EncryptedSingleToken = {
/** Base64-encoded encrypted token data */
encrypted: string;

/** Original token type before encryption */
type: TokenBase['type'];
};

type EncryptedToken = EncryptedSingleToken | Record<string, EncryptedSingleToken>;

export { EncryptToken, EncryptedToken, TokenData, TokenDataWithRef };
83 changes: 43 additions & 40 deletions src/services/tokenEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
EncryptedToken,
EncryptToken,
TokenData,
TokenDataWithRef,
} from '../model/EncryptTokenData';
import { replaceElementRefs } from '../utils/dataManipulationUtils';
import { JWE } from '../utils/jwe';
import { isNilOrEmpty } from '../utils/shared';

export class EncryptValidationError extends Error {
public constructor(message: string) {
Expand Down Expand Up @@ -63,19 +63,25 @@ const createJWE = async (
}
};

const isSingleToken = (tokenRequests: EncryptToken['tokenRequests']) => 'type' in tokenRequests;

/**
* Normalizes token requests to a consistent array format
* @param tokenRequests - Token requests in either single token or object format
* @returns Array of normalized token objects
* Validates if token requests contain valid tokens to encrypt. Tokens must have a type and data property.
* @param tokenRequests - The token requests to validate
* @throws {EncryptValidationError} When token requests are invalid
*/
const normalizeTokenRequests = (
tokenRequests: EncryptToken['tokenRequests']
): Array<TokenDataWithRef> => {
if ('type' in tokenRequests) {
return [tokenRequests as TokenDataWithRef];
const validateTokenRequests = (tokenRequests: EncryptToken['tokenRequests']) => {
if (isNilOrEmpty(tokenRequests)) {
throw new EncryptValidationError('No valid tokens found to encrypt');
}

if (isSingleToken(tokenRequests) && !tokenRequests.data) {
throw new EncryptValidationError('No valid tokens found to encrypt');
}

return Object.values(tokenRequests);
if (!isSingleToken && Object.entries(tokenRequests).some(([_, value]) => !value.data || !value.type)) {
throw new EncryptValidationError('No valid tokens found to encrypt');
}
};

/**
Expand All @@ -86,7 +92,7 @@ const normalizeTokenRequests = (
*/
export const encryptToken = async (
payload: EncryptToken
): Promise<EncryptedToken[]> => {
): Promise<EncryptedToken> => {
if (!payload) {
throw new EncryptValidationError('Encryption payload is required');
}
Expand All @@ -99,40 +105,37 @@ export const encryptToken = async (
throw new EncryptValidationError('Key ID is required');
}

if (!payload.tokenRequests) {
throw new EncryptValidationError('Token requests are required');
}

try {
const tokensWithRef: TokenDataWithRef[] = normalizeTokenRequests(
payload.tokenRequests
);
if (!tokensWithRef.length) {
throw new EncryptValidationError('No valid tokens found to encrypt');
validateTokenRequests(payload.tokenRequests);

if (isSingleToken(payload.tokenRequests)) {
const token = replaceElementRefs<TokenData>(payload.tokenRequests)
const tokenPayload = JSON.stringify(token.data);
const encrypted = await createJWE(tokenPayload, payload.publicKeyPEM, payload.keyId);
return { encrypted, type: token.type };
}

const tokens: TokenData[] = tokensWithRef.map((token) =>
replaceElementRefs<TokenData>(token)
return Object.fromEntries(
await Promise.all(
Object.entries(payload.tokenRequests).map(async ([key, tokenData]) => {
const token = replaceElementRefs<TokenData>(tokenData)

const tokenPayload = JSON.stringify(token.data);
const encrypted = await createJWE(
tokenPayload,
payload.publicKeyPEM,
payload.keyId
);

return [key,
{
encrypted,
type: token.type
}
];
}))
);

return await Promise.all(
tokens.map(async (token) => {
if (!token.type) {
throw new EncryptValidationError('Token type is required');
}

const tokenPayload = JSON.stringify(token.data);
const encrypted = await createJWE(
tokenPayload,
payload.publicKeyPEM,
payload.keyId
);
return {
encrypted,
type: token.type,
};
})
);
} catch (error) {
if (error instanceof EncryptValidationError) {
throw error;
Expand Down
30 changes: 15 additions & 15 deletions tests/modules/tokenEncryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ describe('tokens - Encrypt', () => {

const result = await Tokens({} as BasisTheoryType).encrypt(encryptRequest);
expect(result).toBeDefined();
expect(result).toHaveLength(1);
expect(result![0].encrypted).toBeDefined();
expect(result![0].encrypted.split('.').length).toBe(5);
expect(result![0].type).toBe('card');
const singleResult = result as { encrypted: string; type: string };
expect(singleResult.encrypted).toBeDefined();
expect(singleResult.encrypted.split('.').length).toBe(5);
expect(singleResult.type).toBe('card');
});

test('calls bt encrypt with multiple token requests', async () => {
Expand Down Expand Up @@ -75,13 +75,13 @@ describe('tokens - Encrypt', () => {

const result = await Tokens({} as BasisTheoryType).encrypt(encryptRequest);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
expect(result![0].encrypted).toBeDefined();
expect(result![0].encrypted.split('.').length).toBe(5);
expect(result![0].type).toBe('card');
expect(result![1].encrypted).toBeDefined();
expect(result![1].encrypted.split('.').length).toBe(5);
expect(result![1].type).toBe('bank');
const multipleResult = result as Record<string, { encrypted: string; type: string }>;
expect(multipleResult.card.encrypted).toBeDefined();
expect(multipleResult.card.encrypted.split('.').length).toBe(5);
expect(multipleResult.card.type).toBe('card');
expect(multipleResult.bank.encrypted).toBeDefined();
expect(multipleResult.bank.encrypted.split('.').length).toBe(5);
expect(multipleResult.bank.type).toBe('bank');
});

test('handles nested objects in encrypt requests', async () => {
Expand All @@ -107,10 +107,10 @@ describe('tokens - Encrypt', () => {

const result = await Tokens({} as BasisTheoryType).encrypt(encryptRequest);
expect(result).toBeDefined();
expect(result).toHaveLength(1);
expect(result![0].encrypted).toBeDefined();
expect(result![0].encrypted.split('.').length).toBe(5);
expect(result![0].type).toBe('card');
const singleResult = result as { encrypted: string; type: string };
expect(singleResult.encrypted).toBeDefined();
expect(singleResult.encrypted.split('.').length).toBe(5);
expect(singleResult.type).toBe('card');
});

test('throws error when publicKeyPEM is null', async () => {
Expand Down