emv
Version:
EMV / Chip and PIN CLI and library for PC/SC card readers
235 lines (231 loc) • 7.42 kB
JavaScript
import { parseArgs as nodeParseArgs } from 'node:util';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { createInterface } from 'node:readline';
import { listReaders, waitForCard, selectPse, selectApp, listApps, readRecord, getData, verifyPin, cardInfo, dumpCard, processShellCommand, } from './commands.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Parse command line arguments
*/
export function parseArgs(args) {
const { values, positionals } = nodeParseArgs({
args,
options: {
help: { type: 'boolean', short: 'h' },
version: { type: 'boolean', short: 'v' },
format: { type: 'string', short: 'f' },
verbose: { type: 'boolean' },
reader: { type: 'string', short: 'r' },
},
allowPositionals: true,
});
return {
options: {
help: values.help ?? false,
version: values.version ?? false,
format: values.format,
verbose: values.verbose ?? false,
reader: values.reader,
},
positionals,
};
}
/**
* Show help text
*/
export function showHelp() {
return `EMV CLI - Interact with EMV chip cards
Usage: emv [options] [command] [arguments]
Commands:
(no command) Start interactive mode with beautiful UI
readers List available PC/SC readers
wait Wait for card insertion
info Show card information
select-pse Select Payment System Environment
select-app <aid> Select application by AID
list-apps List applications on card
read-record <sfi> <record> Read a record
get-data <tag> Get data by EMV tag
verify-pin <pin> Verify cardholder PIN
dump Dump all readable card data
shell Text-based interactive mode
Options:
-h, --help Show this help message
-v, --version Show version number
-f, --format <type> Output format: text, json (default: text)
--verbose Show detailed output
-r, --reader <name> Use specific reader by name
Examples:
emv Start interactive UI mode
emv readers List all connected readers
emv wait Wait for a card to be inserted
emv info Show card ATR and basic info
emv select-pse Select PSE directory
emv select-app a0000000041010 Select Mastercard application
emv read-record 1 1 Read record 1 from SFI 1
emv get-data 9f17 Get PIN try counter
emv dump --format json Dump card data as JSON
emv shell Start text-based interactive mode
`;
}
/**
* Get package version
*/
export function showVersion() {
try {
const packagePath = join(__dirname, '..', 'package.json');
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
return pkg.version;
}
catch {
return '0.0.0';
}
}
/**
* Create command context from parsed options
*/
function createContext(options) {
return {
output: (msg) => {
console.log(msg);
},
error: (msg) => {
console.error(msg);
},
readerName: options.reader,
format: options.format,
verbose: options.verbose,
};
}
/**
* Run a command and handle errors
*/
export async function runCommand(command, args, ctx) {
switch (command) {
case 'readers':
return listReaders(ctx);
case 'wait':
return waitForCard(ctx);
case 'select-pse':
return selectPse(ctx);
case 'select-app': {
const aid = args[0];
if (!aid) {
ctx.error('Usage: emv select-app <aid>');
return 1;
}
return selectApp(ctx, aid);
}
case 'list-apps':
return listApps(ctx);
case 'read-record': {
const sfiArg = args[0];
const recordArg = args[1];
if (!sfiArg || !recordArg) {
ctx.error('Usage: emv read-record <sfi> <record>');
return 1;
}
const sfi = parseInt(sfiArg, 10);
const record = parseInt(recordArg, 10);
if (Number.isNaN(sfi) || Number.isNaN(record)) {
ctx.error('SFI and record must be numbers');
return 1;
}
return readRecord(ctx, sfi, record);
}
case 'get-data': {
const tagArg = args[0];
if (!tagArg) {
ctx.error('Usage: emv get-data <tag>');
return 1;
}
return getData(ctx, tagArg);
}
case 'verify-pin': {
const pin = args[0];
if (!pin) {
ctx.error('Usage: emv verify-pin <pin>');
return 1;
}
return verifyPin(ctx, pin);
}
case 'info':
return cardInfo(ctx);
case 'dump':
return dumpCard(ctx);
case 'shell':
return runShell(ctx);
default:
ctx.error(`Command '${command}' not yet implemented`);
return 1;
}
}
/**
* Run interactive shell mode
*/
async function runShell(ctx) {
ctx.output('EMV Interactive Shell');
ctx.output('Type "help" for available commands, "quit" to exit');
ctx.output('');
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
const prompt = () => {
rl.question('emv> ', (input) => {
void (async () => {
try {
const result = await processShellCommand(ctx, input);
if (result.action === 'exit') {
rl.close();
resolve(0);
return;
}
prompt();
}
catch (error) {
ctx.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
prompt();
}
})();
});
};
rl.on('close', () => {
resolve(0);
});
prompt();
});
}
/**
* Main CLI entry point
*/
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.options.help) {
console.log(showHelp());
return;
}
if (args.options.version) {
console.log(showVersion());
return;
}
const command = args.positionals[0];
if (!command) {
// Default to interactive mode when no command given
const { runInteractive } = await import('./interactive.js');
runInteractive();
return;
}
const ctx = createContext(args.options);
const commandArgs = args.positionals.slice(1);
process.exitCode = await runCommand(command, commandArgs, ctx);
}
main().catch((error) => {
console.error('Error:', error instanceof Error ? error.message : error);
process.exitCode = 1;
});
//# sourceMappingURL=cli.js.map