UNPKG

@kodeglot/node-python-eid-reader

Version:

A cross-platform Node.js package to read Belgian eID card public data using Python eidreader with automatic dependency checking

699 lines 32.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.EidReader = exports.EidReaderError = void 0; exports.readEidData = readEidData; exports.checkRequirements = checkRequirements; exports.installRequirements = installRequirements; exports.checkEidReaderAvailable = checkEidReaderAvailable; exports.checkCardInserted = checkCardInserted; exports.createPcscDetector = createPcscDetector; exports.getPcscStatus = getPcscStatus; exports.isPcscReaderAvailable = isPcscReaderAvailable; exports.isPcscCardInserted = isPcscCardInserted; const child_process_1 = require("child_process"); const os = __importStar(require("os")); const requirements_checker_1 = require("./requirements-checker"); const pcsc_detect_1 = require("./pcsc-detect"); class EidReaderError extends Error { constructor(message, code) { super(message); this.code = code; this.name = 'EidReaderError'; } } exports.EidReaderError = EidReaderError; class EidReader { constructor(options = {}) { this.info = []; this.options = { verbose: options.verbose ?? false, maxRetries: options.maxRetries ?? 3, retryDelay: options.retryDelay ?? 300, pythonPath: options.pythonPath ?? '', pkcs11LibPath: options.pkcs11LibPath ?? '' }; this.requirementsChecker = new requirements_checker_1.RequirementsChecker(); } addInfo(msg) { this.info.push(msg); if (this.options.verbose) { // Optionally log for debugging // console.log(msg); } } async checkRequirements() { this.info = []; try { this.addInfo('Checking system requirements...'); const result = await this.requirementsChecker.checkAllRequirements(); if (result.passed) { this.addInfo('All requirements are met!'); } else { this.addInfo('Some requirements are missing.'); } return { success: result.passed, info: [...this.info], data: result }; } catch (error) { return { success: false, info: [...this.info], error: { message: error.message } }; } } async installRequirements() { this.info = []; try { this.addInfo('Checking for missing requirements...'); const { results, passed } = await this.requirementsChecker.checkAllRequirements(); if (passed) { this.addInfo('All requirements are already available!'); return { success: true, info: [...this.info], data: { installed: true } }; } const installed = await this.requirementsChecker.installMissingRequirements(results); if (installed) { this.addInfo('Requirements installed successfully!'); } else { this.addInfo('Some requirements could not be installed automatically.'); } return { success: installed, info: [...this.info], data: { installed } }; } catch (error) { return { success: false, info: [...this.info], error: { message: error.message } }; } } async checkEidReaderAvailable() { this.info = []; try { this.addInfo('Checking eID reader availability...'); // First check if requirements are met const reqResult = await this.checkRequirements(); if (!reqResult.success) { this.addInfo('Requirements not met, eID reader not available'); return { success: false, info: [...this.info], error: reqResult.error }; } // Try to find Python with eidreader let pythonCmd; try { pythonCmd = await this.findPythonWithEidreader(); this.addInfo(`Python found: ${pythonCmd}`); } catch (error) { this.addInfo('Python with eidreader not found'); return { success: false, info: [...this.info], error: { message: error.message } }; } // Try to actually read from a reader to check if one is connected try { const env = this.setupEnvironment(); this.addInfo('Environment configured successfully'); // Try to read from a reader with a short timeout const testResult = await new Promise((resolve) => { const testProc = (0, child_process_1.spawn)(pythonCmd, ['-m', 'eidreader'], { stdio: ['pipe', 'pipe', 'pipe'], env, }); let stdout = ''; let stderr = ''; testProc.stdout.on('data', (data) => { stdout += data.toString(); }); testProc.stderr.on('data', (data) => { stderr += data.toString(); }); testProc.on('close', (code) => { // LOGGING: Print raw output for debugging if (this.options.verbose || process.env.EIDREADER_DEBUG) { console.log('[checkEidReaderAvailable] process closed with code:', code); console.log('[checkEidReaderAvailable] stdout:', stdout); console.log('[checkEidReaderAvailable] stderr:', stderr); } // If we get valid JSON output, parse it to check the success field if (stdout.trim() && stdout.includes('{')) { try { const jsonLine = stdout.split('\n').find(line => line.trim().startsWith('{')); if (jsonLine) { const data = JSON.parse(jsonLine); // If success is true, reader is available and card is inserted if (data.success === true) { resolve({ available: true, stderr, stdout }); return; } // If success is false, check the message to determine if it's reader or card issue if (data.success === false && data.message) { const message = data.message.toLowerCase(); // Check for reader-related errors const readerErrors = [ 'no reader', 'no readers', 'no device', 'no smartcard reader', 'no card reader', 'no smart card reader', 'no pc/sc readers', 'no readers available', 'no readers detected', 'no smart card reader detected', 'could not find any reader', 'no reader found', 'no device found', ]; // Check for card-related errors (reader is available but no card) const cardErrors = [ 'ckr_device_error', 'no card', 'card not present', 'no card found', 'card not inserted', 'no smart card inserted', 'no card in reader', 'no card present', 'no card detected', 'could not find any reader with a card inserted', ]; const hasCardError = cardErrors.some(err => message.includes(err)); if (hasCardError) { resolve({ available: true, stderr, stdout }); return; } const hasReaderError = readerErrors.some(err => message.includes(err)); if (hasReaderError) { resolve({ available: false, stderr, stdout }); return; } // If it's not a reader error, assume reader is available but no card resolve({ available: true, stderr, stdout }); return; } // If we can't parse the success field, assume available resolve({ available: true, stderr, stdout }); return; } } catch { } } // Check for known no-reader errors in stderr or stdout const noReaderErrors = [ 'No reader found', 'No device found', 'No smartcard reader found', 'No readers found', 'No reader connected', 'No PC/SC readers', 'No readers available', 'No smart card readers found', 'No card readers found', 'No card reader found', 'No readers detected', 'No smart card reader detected', ]; const hasNoReaderError = noReaderErrors.some(err => stderr.toLowerCase().includes(err.toLowerCase()) || stdout.toLowerCase().includes(err.toLowerCase())); // If we get a no-reader error, reader is not available if (hasNoReaderError) { resolve({ available: false, stderr, stdout }); return; } // If we get card-related errors (like "no card"), reader is available const cardErrors = [ 'no card', 'card not present', 'no card found', 'card not inserted', 'no smart card inserted', 'no card in reader', 'no card present', 'no card detected', ]; const hasCardError = cardErrors.some(err => stderr.toLowerCase().includes(err.toLowerCase()) || stdout.toLowerCase().includes(err.toLowerCase())); if (hasCardError) { resolve({ available: true, stderr, stdout }); return; } // Default to not available if we can't determine resolve({ available: false, stderr, stdout }); }); testProc.on('error', (error) => { if (this.options.verbose || process.env.EIDREADER_DEBUG) { console.log('[checkEidReaderAvailable] process error:', error); } resolve({ available: false, stderr: '', stdout: '' }); }); // Timeout after 10 seconds setTimeout(() => { testProc.kill(); resolve({ available: false, stderr, stdout }); }, 10000); }); if (testResult.available) { this.addInfo('eID reader is available'); return { success: true, info: [...this.info], data: { available: true, readerType: 'hardware' } }; } else { this.addInfo('No eID reader detected'); return { success: true, info: [...this.info], data: { available: false } }; } } catch (error) { this.addInfo('Failed to test eID reader'); return { success: false, info: [...this.info], error: { message: error.message } }; } } catch (error) { return { success: false, info: [...this.info], error: { message: error.message } }; } } async checkCardInserted() { this.info = []; try { this.addInfo('Checking for inserted eID card...'); // First check if reader is available const readerResult = await this.checkEidReaderAvailable(); if (!readerResult.success || !readerResult.data?.available) { this.addInfo('eID reader not available'); return { success: false, info: [...this.info], error: readerResult.error }; } // Try to read card data with a short timeout try { const pythonCmd = await this.findPythonWithEidreader(); const env = this.setupEnvironment(); const cardResult = await new Promise((resolve) => { const cardProc = (0, child_process_1.spawn)(pythonCmd, ['-m', 'eidreader'], { stdio: ['pipe', 'pipe', 'pipe'], env, }); let stdout = ''; let stderr = ''; let hasOutput = false; cardProc.stdout.on('data', (data) => { stdout += data.toString(); hasOutput = true; }); cardProc.stderr.on('data', (data) => { stderr += data.toString(); hasOutput = true; }); cardProc.on('close', (code) => { // LOGGING: Print raw output for debugging if (this.options.verbose || process.env.EIDREADER_DEBUG) { console.log('[checkCardInserted] process closed with code:', code); console.log('[checkCardInserted] stdout:', stdout); console.log('[checkCardInserted] stderr:', stderr); } // If we get valid JSON output, parse it to check the success field if (stdout.trim() && stdout.includes('{')) { try { const jsonLine = stdout.split('\n').find(line => line.trim().startsWith('{')); if (jsonLine) { const data = JSON.parse(jsonLine); // If success is true, card is inserted if (data.success === true) { resolve({ inserted: true, data, stderr, stdout }); return; } // If success is false, card is not inserted if (data.success === false) { resolve({ inserted: false, stderr, stdout }); return; } // If we can't determine, assume not inserted resolve({ inserted: false, stderr, stdout }); return; } } catch { } } // Check for known no-card errors const noCardErrors = [ 'CKR_DEVICE_ERROR', 'No card found', 'Card not present', 'No reader found', 'No device found', 'No card inserted', 'No smart card inserted', 'No card in reader', 'No card present', 'No card detected', ]; const hasNoCardError = noCardErrors.some(err => stderr.includes(err) || stdout.includes(err)); if (hasNoCardError) { resolve({ inserted: false, stderr, stdout }); return; } resolve({ inserted: false, stderr, stdout }); }); cardProc.on('error', (error) => { if (this.options.verbose || process.env.EIDREADER_DEBUG) { console.log('[checkCardInserted] process error:', error); } resolve({ inserted: false, stderr: '', stdout: '' }); }); // Timeout after 10 seconds setTimeout(() => { cardProc.kill(); resolve({ inserted: false, stderr, stdout }); }, 10000); }); if (cardResult.inserted && cardResult.data) { this.addInfo('eID card detected and readable'); return { success: true, info: [...this.info], data: { inserted: true, cardInfo: { cardNumber: cardResult.data.card_number, name: `${cardResult.data.firstnames || ''} ${cardResult.data.surname || ''}`.trim() } } }; } else { this.addInfo('No eID card detected'); return { success: true, info: [...this.info], data: { inserted: false } }; } } catch (error) { this.addInfo('Failed to check for card'); return { success: false, info: [...this.info], error: { message: error.message } }; } } catch (error) { return { success: false, info: [...this.info], error: { message: error.message } }; } } async readEidData() { this.info = []; try { this.addInfo('Checking requirements before reading eID data...'); const reqResult = await this.checkRequirements(); if (!reqResult.success) { return { success: false, info: [...this.info, ...reqResult.info], error: reqResult.error }; } this.addInfo('Requirements OK. Starting eID read...'); const pythonCmd = await this.findPythonWithEidreader(); this.addInfo(`Using Python: ${pythonCmd}`); const env = this.setupEnvironment(); this.addInfo(`Environment configured for ${os.platform()}`); const output = await this.runEidReader(pythonCmd, env); const eidData = this.parseEidreaderOutput(output); this.addInfo('Successfully read and parsed eID data.'); return { success: true, info: [...this.info], data: eidData }; } catch (error) { const err = error instanceof EidReaderError ? { message: error.message, code: error.code } : { message: error.message }; return { success: false, info: [...this.info], error: err }; } } async findPythonWithEidreader() { if (this.options.pythonPath) { const isValid = await this.tryPythonCommand(this.options.pythonPath); if (isValid) return this.options.pythonPath; throw new EidReaderError(`Invalid Python path: ${this.options.pythonPath}`); } const candidates = os.platform() === 'win32' ? ['python', 'python3'] : ['/usr/bin/python3', '/opt/homebrew/bin/python3', 'python3', 'python']; for (const cmd of candidates) { const found = await this.tryPythonCommand(cmd); if (found) return found; } throw new EidReaderError('Could not find a Python interpreter with eidreader installed. Please install Python 3.x and eidreader package.', 'PYTHON_NOT_FOUND'); } async tryPythonCommand(pythonCmd) { return new Promise((resolve) => { const proc = (0, child_process_1.spawn)(pythonCmd, ['-m', 'eidreader', '--help']); let output = ''; let error = ''; proc.stdout.on('data', (data) => (output += data.toString())); proc.stderr.on('data', (data) => (error += data.toString())); proc.on('close', (code) => { if (code === 0 && output.includes('eidreader')) { resolve(pythonCmd); } else { resolve(null); } }); proc.on('error', () => resolve(null)); }); } setupEnvironment() { const env = { ...process.env }; if (os.platform() === 'darwin') { const libraryPaths = [ '/Library/Belgium Identity Card/Pkcs11', '/Library/Belgium Identity Card/Pkcs11/beid-pkcs11.bundle/Contents/MacOS', '/usr/local/lib', '/usr/lib' ]; const existingPath = env.DYLD_LIBRARY_PATH || ''; env.DYLD_LIBRARY_PATH = [...libraryPaths, existingPath].filter(Boolean).join(':'); } if (this.options.pkcs11LibPath) { env.PYKCS11LIB = this.options.pkcs11LibPath; } else if (process.env.PYKCS11LIB) { env.PYKCS11LIB = process.env.PYKCS11LIB; } else if (os.platform() === 'darwin') { env.PYKCS11LIB = '/Library/Belgium Identity Card/Pkcs11/libbeidpkcs11.dylib'; } else if (os.platform() === 'win32') { env.PYKCS11LIB = 'C:\\Program Files\\Belgium Identity Card\\pkcs11\\beidpkcs11.dll'; } else if (os.platform() === 'linux') { env.PYKCS11LIB = 'libbeidpkcs11.so.0'; } return env; } async runEidReader(pythonCmd, env) { for (let attempt = 1; attempt <= this.options.maxRetries; attempt++) { try { this.addInfo(`Running eID reader (attempt ${attempt}/${this.options.maxRetries})...`); const result = await new Promise((resolve, reject) => { const eidReader = (0, child_process_1.spawn)(pythonCmd, ['-m', 'eidreader'], { stdio: ['pipe', 'pipe', 'pipe'], env, }); let stdout = ''; let stderr = ''; eidReader.stdout.on('data', (data) => { stdout += data.toString(); }); eidReader.stderr.on('data', (data) => { stderr += data.toString(); }); eidReader.on('close', (code) => { if (code === 0) { resolve(stdout); } else { reject(new EidReaderError(`eidreader failed with code ${code}: ${stderr}`, 'EIDREADER_FAILED')); } }); eidReader.on('error', (error) => { reject(new EidReaderError(`Failed to start eidreader: ${error.message}`, 'SPAWN_ERROR')); }); }); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('CKR_DEVICE_ERROR') && attempt < this.options.maxRetries) { this.addInfo(`Device error detected, retrying in ${this.options.retryDelay}ms...`); await new Promise(resolve => setTimeout(resolve, this.options.retryDelay)); continue; } throw error; } } throw new EidReaderError('Max retries exceeded', 'MAX_RETRIES_EXCEEDED'); } parseEidreaderOutput(output) { const jsonLine = output.split('\n').find(line => line.trim().startsWith('{')); if (!jsonLine) { throw new EidReaderError('No JSON output from eidreader', 'INVALID_OUTPUT'); } try { const data = JSON.parse(jsonLine); return { cardNumber: data.card_number || "", chipNumber: data.chip_number || "", validityDateBegin: data.validity_begin_date || "", validityDateEnd: data.validity_end_date || "", municipality: data.issuing_municipality || "", nationality: data.nationality || "", birthLocation: data.location_of_birth || "", birthDate: data.date_of_birth || "", name: data.surname || "", firstName: data.firstnames || "", sex: data.gender || "", documentType: data.document_type || "", address: { streetAndNumber: data.address_street_and_number || "", zip: data.address_zip || "", municipality: data.address_municipality || "" }, photo: data.PHOTO_FILE || "" }; } catch (error) { throw new EidReaderError(`Failed to parse eID data: ${error instanceof Error ? error.message : String(error)}`, 'PARSE_ERROR'); } } } exports.EidReader = EidReader; async function readEidData(options) { const reader = new EidReader(options); return await reader.readEidData(); } async function checkRequirements() { const reader = new EidReader(); return await reader.checkRequirements(); } async function installRequirements() { const reader = new EidReader(); return await reader.installRequirements(); } async function checkEidReaderAvailable() { const reader = new EidReader(); return await reader.checkEidReaderAvailable(); } async function checkCardInserted() { const reader = new EidReader(); return await reader.checkCardInserted(); } /** * Create and start a PC/SC detector for real-time reader/card monitoring * @param options Configuration options * @returns Promise that resolves to a PcscDetector instance */ async function createPcscDetector() { const detector = new pcsc_detect_1.PcscDetector(); // Wait a moment for initial setup await new Promise(resolve => setTimeout(resolve, 100)); return detector; } /** * Get current PC/SC status (readers and cards) * @returns Promise that resolves to current PC/SC status */ async function getPcscStatus() { try { const detector = new pcsc_detect_1.PcscDetector(); // Wait for initial setup await new Promise(resolve => setTimeout(resolve, 200)); const status = detector.getStatus(); detector.close(); return { success: true, info: ['PC/SC status retrieved successfully'], data: status }; } catch (error) { return { success: false, info: ['Failed to get PC/SC status'], error: { message: error.message } }; } } /** * Check if any PC/SC reader is available * @returns Promise that resolves to reader availability status */ async function isPcscReaderAvailable() { try { const statusResult = await getPcscStatus(); if (!statusResult.success) { return { success: false, info: statusResult.info, error: statusResult.error, data: { available: false } }; } const available = statusResult.data.readers.length > 0; return { success: true, info: available ? ['PC/SC reader available'] : ['No PC/SC reader available'], data: { available } }; } catch (error) { return { success: false, info: ['Failed to check PC/SC reader availability'], error: { message: error.message }, data: { available: false } }; } } /** * Check if any card is inserted in any PC/SC reader * @returns Promise that resolves to card insertion status */ async function isPcscCardInserted() { try { const statusResult = await getPcscStatus(); if (!statusResult.success) { return { success: false, info: statusResult.info, error: statusResult.error, data: { inserted: false } }; } const cardReader = statusResult.data.readers.find((reader) => reader.cardPresent); const inserted = !!cardReader; return { success: true, info: inserted ? ['Card inserted'] : ['No card inserted'], data: { inserted, readerName: cardReader?.name } }; } catch (error) { return { success: false, info: ['Failed to check PC/SC card insertion'], error: { message: error.message }, data: { inserted: false } }; } } //# sourceMappingURL=index.js.map