Skip to content

Conversation

@tomkp
Copy link
Owner

@tomkp tomkp commented Dec 30, 2025

Summary

  • Adds parseCvmList() function to parse EMV tag 8E (CVM List) data
  • Adds evaluateCvm() function to select the appropriate verification method based on transaction context
  • Supports all standard CVM conditions including terminal support, amount thresholds, cash transactions, etc.
  • Exports new types: CvmMethod, CvmCondition, CvmRule, CvmList, CvmContext

Test plan

  • Unit tests for parseCvmList() parsing various CVM list formats
  • Unit tests for evaluateCvm() with different contexts and conditions
  • All 172 tests pass
  • Lint checks pass

Closes #104

- Add parseCvmList() to parse EMV tag 8E CVM List data
- Add evaluateCvm() to select appropriate verification method based on context
- Support all standard CVM conditions (terminal support, amount thresholds, etc.)
- Export CvmMethod, CvmCondition, CvmRule, CvmList, CvmContext types
- Add comprehensive tests for CVM parsing and evaluation

Closes #104
Copilot AI review requested due to automatic review settings December 30, 2025 14:42
@tomkp tomkp merged commit 849e193 into master Dec 30, 2025
6 checks passed
@tomkp tomkp deleted the feature/cvm-evaluation branch December 30, 2025 14:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds CVM (Cardholder Verification Method) parsing and evaluation functionality to the EMV library. It introduces the ability to parse EMV tag 8E (CVM List) data and evaluate CVM rules against transaction contexts to select the appropriate verification method.

  • Adds parseCvmList() function to decode CVM List data structure with amount thresholds and verification rules
  • Adds evaluateCvm() function to select appropriate verification methods based on transaction context
  • Exports five new types: CvmMethod, CvmCondition, CvmRule, CvmList, and CvmContext

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/index.ts Exports new CVM-related functions (parseCvmList, evaluateCvm) and types (CvmMethod, CvmCondition, CvmRule, CvmList, CvmContext)
src/emv-application.ts Implements CVM parsing logic with bit manipulation for method codes and condition bytes, plus evaluation logic for 11 different condition types
src/emv-application.test.ts Adds test suites for both parsing and evaluation functions covering basic scenarios, amount thresholds, and fail-on-unsuccessful flag handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +944 to +998
describe('evaluateCvm', async () => {
const { parseCvmList, evaluateCvm } = await import('./emv-application.js');

it('should select first matching rule', () => {
const cvmList = parseCvmList(Buffer.from([
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x03, // Enciphered PIN, if terminal supports
0x1e, 0x03, // Signature, if terminal supports
]));

const result = evaluateCvm(cvmList, { terminalSupportsCvm: true });
assert.strictEqual(result?.method, 'enciphered_pin_online');
});

it('should skip rules where condition not met', () => {
const cvmList = parseCvmList(Buffer.from([
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x03, // Enciphered PIN, if terminal supports
0x1f, 0x00, // No CVM, always
]));

const result = evaluateCvm(cvmList, { terminalSupportsCvm: false });
assert.strictEqual(result?.method, 'no_cvm');
});

it('should handle amount threshold conditions', () => {
const cvmList = parseCvmList(Buffer.from([
0x00, 0x00, 0x03, 0xe8, // X = 1000
0x00, 0x00, 0x00, 0x00, // Y = 0
0x02, 0x07, // Enciphered PIN, if amount > X
0x1f, 0x00, // No CVM, always
]));

// Amount 500 is under X (1000), so PIN rule doesn't apply
const result1 = evaluateCvm(cvmList, { amount: 500 });
assert.strictEqual(result1?.method, 'no_cvm');

// Amount 1500 is over X, so PIN rule applies
const result2 = evaluateCvm(cvmList, { amount: 1500 });
assert.strictEqual(result2?.method, 'enciphered_pin_online');
});

it('should return undefined if no rules match', () => {
const cvmList = parseCvmList(Buffer.from([
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x03, // Enciphered PIN, if terminal supports
]));

const result = evaluateCvm(cvmList, { terminalSupportsCvm: false });
assert.strictEqual(result, undefined);
});
});
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new CVM evaluation logic lacks test coverage for several condition types. While the implementation includes logic for conditions like 'unattended_cash', 'manual_cash', 'purchase_with_cashback', 'not_unattended_cash_manual_pin', and the 'amount_under_x/y' conditions, these are not tested. Consider adding test cases that verify these conditions work correctly, especially the complex boolean logic in 'not_unattended_cash_manual_pin'.

Copilot uses AI. Check for mistakes.
Comment on lines +900 to +942
describe('parseCvmList', async () => {
const { parseCvmList } = await import('./emv-application.js');

it('should parse CVM list with amount thresholds', () => {
// CVM List: X=1000, Y=5000, then rules
const buffer = Buffer.from([
0x00, 0x00, 0x03, 0xe8, // X = 1000
0x00, 0x00, 0x13, 0x88, // Y = 5000
0x02, 0x03, // Enciphered PIN online, if terminal supports CVM
0x1e, 0x03, // Signature, if terminal supports CVM
0x1f, 0x00, // No CVM, always
]);
const result = parseCvmList(buffer);

assert.strictEqual(result.amountX, 1000);
assert.strictEqual(result.amountY, 5000);
assert.strictEqual(result.rules.length, 3);

assert.strictEqual(result.rules[0]?.method, 'enciphered_pin_online');
assert.strictEqual(result.rules[0]?.condition, 'terminal_supports_cvm');
assert.strictEqual(result.rules[0]?.failIfUnsuccessful, true);

assert.strictEqual(result.rules[1]?.method, 'signature');
assert.strictEqual(result.rules[2]?.method, 'no_cvm');
});

it('should handle continue-on-fail flag', () => {
const buffer = Buffer.from([
0x00, 0x00, 0x00, 0x00, // X = 0
0x00, 0x00, 0x00, 0x00, // Y = 0
0x42, 0x00, // Enciphered PIN online + continue if fails, always
]);
const result = parseCvmList(buffer);

assert.strictEqual(result.rules[0]?.failIfUnsuccessful, false);
});

it('should return empty rules for buffer too short', () => {
const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
const result = parseCvmList(buffer);
assert.strictEqual(result.rules.length, 0);
});
});
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case for buffers with incomplete CVM rules (odd number of bytes after the 8-byte header). While the implementation correctly handles this by checking 'i + 1 < buffer.length', there's no test verifying this edge case. Consider adding a test with a 9-byte buffer to ensure incomplete rules are silently skipped.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add CVM (Cardholder Verification Method) evaluation

2 participants