smartcard 1.0.46 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 tomkp
3
+ Copyright (c) 2025 Tom KP
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md ADDED
@@ -0,0 +1,387 @@
1
+ # smartcard
2
+
3
+ Modern PC/SC (Personal Computer/Smart Card) bindings for Node.js using N-API.
4
+
5
+ Unlike older NAN-based bindings that break with each Node.js major version, this library uses N-API for ABI stability across Node.js versions 12, 14, 16, 18, 20, 22, 24, and beyond - without recompilation.
6
+
7
+ ## Features
8
+
9
+ - **ABI Stable**: Works across Node.js versions without recompilation
10
+ - **Async/Promise-based**: Non-blocking card operations
11
+ - **Event-driven API**: High-level `Devices` class with EventEmitter
12
+ - **TypeScript support**: Full type definitions included
13
+ - **Cross-platform**: Windows, macOS, and Linux
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install smartcard
19
+ ```
20
+
21
+ ### Prerequisites
22
+
23
+ **macOS**: No additional setup required (uses built-in PCSC.framework)
24
+
25
+ **Windows**: No additional setup required (uses built-in winscard.dll)
26
+
27
+ **Linux**:
28
+ ```bash
29
+ # Debian/Ubuntu
30
+ sudo apt-get install libpcsclite-dev pcscd
31
+
32
+ # Fedora/RHEL
33
+ sudo dnf install pcsc-lite-devel pcsc-lite
34
+
35
+ # Start the PC/SC daemon
36
+ sudo systemctl start pcscd
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### High-Level API (Event-Driven)
42
+
43
+ ```javascript
44
+ const { Devices } = require('smartcard');
45
+
46
+ const devices = new Devices();
47
+
48
+ devices.on('reader-attached', (reader) => {
49
+ console.log(`Reader attached: ${reader.name}`);
50
+ });
51
+
52
+ devices.on('reader-detached', (reader) => {
53
+ console.log(`Reader detached: ${reader.name}`);
54
+ });
55
+
56
+ devices.on('card-inserted', async ({ reader, card }) => {
57
+ console.log(`Card inserted in ${reader.name}`);
58
+ console.log(` ATR: ${card.atr.toString('hex')}`);
59
+
60
+ // Send APDU command
61
+ try {
62
+ const response = await card.transmit([0xFF, 0xCA, 0x00, 0x00, 0x00]);
63
+ console.log(` UID: ${response.slice(0, -2).toString('hex')}`);
64
+ } catch (err) {
65
+ console.error('Transmit error:', err.message);
66
+ }
67
+ });
68
+
69
+ devices.on('card-removed', ({ reader }) => {
70
+ console.log(`Card removed from ${reader.name}`);
71
+ });
72
+
73
+ devices.on('error', (err) => {
74
+ console.error('Error:', err.message);
75
+ });
76
+
77
+ // Start monitoring
78
+ devices.start();
79
+
80
+ // Stop on exit
81
+ process.on('SIGINT', () => {
82
+ devices.stop();
83
+ process.exit();
84
+ });
85
+ ```
86
+
87
+ ### Low-Level API (Direct PC/SC)
88
+
89
+ ```javascript
90
+ const {
91
+ Context,
92
+ SCARD_SHARE_SHARED,
93
+ SCARD_PROTOCOL_T0,
94
+ SCARD_PROTOCOL_T1,
95
+ SCARD_LEAVE_CARD
96
+ } = require('smartcard');
97
+
98
+ async function main() {
99
+ // Create PC/SC context
100
+ const ctx = new Context();
101
+ console.log('Context valid:', ctx.isValid);
102
+
103
+ // List readers
104
+ const readers = ctx.listReaders();
105
+ console.log('Readers:', readers.map(r => r.name));
106
+
107
+ if (readers.length === 0) {
108
+ console.log('No readers found');
109
+ ctx.close();
110
+ return;
111
+ }
112
+
113
+ const reader = readers[0];
114
+ console.log(`Using reader: ${reader.name}`);
115
+ console.log(` State: ${reader.state}`);
116
+
117
+ // Connect to card
118
+ try {
119
+ const card = await reader.connect(
120
+ SCARD_SHARE_SHARED,
121
+ SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1
122
+ );
123
+ console.log(`Connected, protocol: ${card.protocol}`);
124
+
125
+ // Get card status
126
+ const status = card.getStatus();
127
+ console.log(` ATR: ${status.atr.toString('hex')}`);
128
+
129
+ // Send APDU (Get UID for contactless cards)
130
+ const response = await card.transmit(Buffer.from([0xFF, 0xCA, 0x00, 0x00, 0x00]));
131
+ console.log(` Response: ${response.toString('hex')}`);
132
+
133
+ // Disconnect
134
+ card.disconnect(SCARD_LEAVE_CARD);
135
+ } catch (err) {
136
+ console.error('Card error:', err.message);
137
+ }
138
+
139
+ // Close context
140
+ ctx.close();
141
+ }
142
+
143
+ main();
144
+ ```
145
+
146
+ ### Waiting for Card Changes
147
+
148
+ ```javascript
149
+ const { Context } = require('smartcard');
150
+
151
+ async function waitForCard() {
152
+ const ctx = new Context();
153
+ const readers = ctx.listReaders();
154
+
155
+ if (readers.length === 0) {
156
+ console.log('No readers found');
157
+ ctx.close();
158
+ return;
159
+ }
160
+
161
+ console.log('Waiting for card...');
162
+
163
+ // Wait for state change (timeout: 30 seconds)
164
+ const changes = await ctx.waitForChange(readers, 30000);
165
+
166
+ if (changes === null) {
167
+ console.log('Cancelled');
168
+ } else if (changes.length === 0) {
169
+ console.log('Timeout');
170
+ } else {
171
+ for (const change of changes) {
172
+ if (change.changed) {
173
+ console.log(`${change.name}: state changed to ${change.state}`);
174
+ if (change.atr) {
175
+ console.log(` ATR: ${change.atr.toString('hex')}`);
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ ctx.close();
182
+ }
183
+
184
+ waitForCard();
185
+ ```
186
+
187
+ ## API Reference
188
+
189
+ ### Context
190
+
191
+ The low-level PC/SC context.
192
+
193
+ ```typescript
194
+ class Context {
195
+ constructor();
196
+ readonly isValid: boolean;
197
+ listReaders(): Reader[];
198
+ waitForChange(readers?: Reader[], timeout?: number): Promise<ReaderState[] | null>;
199
+ cancel(): void;
200
+ close(): void;
201
+ }
202
+ ```
203
+
204
+ ### Reader
205
+
206
+ Represents a smart card reader.
207
+
208
+ ```typescript
209
+ interface Reader {
210
+ readonly name: string;
211
+ readonly state: number;
212
+ readonly atr: Buffer | null;
213
+ connect(shareMode?: number, protocol?: number): Promise<Card>;
214
+ }
215
+ ```
216
+
217
+ ### Card
218
+
219
+ Represents a connected smart card.
220
+
221
+ ```typescript
222
+ interface Card {
223
+ readonly protocol: number;
224
+ readonly connected: boolean;
225
+ readonly atr: Buffer | null;
226
+ transmit(command: Buffer | number[]): Promise<Buffer>;
227
+ control(code: number, data?: Buffer): Promise<Buffer>;
228
+ getStatus(): { state: number; protocol: number; atr: Buffer };
229
+ disconnect(disposition?: number): void;
230
+ reconnect(shareMode?: number, protocol?: number, init?: number): number;
231
+ }
232
+ ```
233
+
234
+ ### Devices
235
+
236
+ High-level event-driven API.
237
+
238
+ ```typescript
239
+ class Devices extends EventEmitter {
240
+ start(): void;
241
+ stop(): void;
242
+ listReaders(): Reader[];
243
+
244
+ on(event: 'reader-attached', listener: (reader: Reader) => void): this;
245
+ on(event: 'reader-detached', listener: (reader: Reader) => void): this;
246
+ on(event: 'card-inserted', listener: (event: { reader: Reader; card: Card }) => void): this;
247
+ on(event: 'card-removed', listener: (event: { reader: Reader; card: Card | null }) => void): this;
248
+ on(event: 'error', listener: (error: Error) => void): this;
249
+ }
250
+ ```
251
+
252
+ ### Constants
253
+
254
+ ```javascript
255
+ // Share modes
256
+ SCARD_SHARE_EXCLUSIVE // Exclusive access
257
+ SCARD_SHARE_SHARED // Shared access (default)
258
+ SCARD_SHARE_DIRECT // Direct access to reader
259
+
260
+ // Protocols
261
+ SCARD_PROTOCOL_T0 // T=0 protocol
262
+ SCARD_PROTOCOL_T1 // T=1 protocol
263
+ SCARD_PROTOCOL_RAW // Raw protocol
264
+
265
+ // Disposition (for disconnect)
266
+ SCARD_LEAVE_CARD // Leave card as-is
267
+ SCARD_RESET_CARD // Reset the card
268
+ SCARD_UNPOWER_CARD // Power down the card
269
+ SCARD_EJECT_CARD // Eject the card
270
+
271
+ // State flags
272
+ SCARD_STATE_PRESENT // Card is present
273
+ SCARD_STATE_EMPTY // No card in reader
274
+ SCARD_STATE_CHANGED // State has changed
275
+ // ... and more
276
+ ```
277
+
278
+ ## Common APDU Commands
279
+
280
+ ```javascript
281
+ // Get UID (for contactless cards via PC/SC pseudo-APDU)
282
+ const GET_UID = [0xFF, 0xCA, 0x00, 0x00, 0x00];
283
+
284
+ // Select by AID
285
+ const SELECT_AID = [0x00, 0xA4, 0x04, 0x00, /* length */, /* AID bytes */];
286
+
287
+ // Read binary
288
+ const READ_BINARY = [0x00, 0xB0, /* P1: offset high */, /* P2: offset low */, /* Le */];
289
+ ```
290
+
291
+ ## Error Handling
292
+
293
+ ```javascript
294
+ const { PCSCError, CardRemovedError, TimeoutError } = require('smartcard');
295
+
296
+ try {
297
+ const response = await card.transmit([0x00, 0xA4, 0x04, 0x00]);
298
+ } catch (err) {
299
+ if (err instanceof CardRemovedError) {
300
+ console.log('Card was removed');
301
+ } else if (err instanceof TimeoutError) {
302
+ console.log('Operation timed out');
303
+ } else if (err instanceof PCSCError) {
304
+ console.log(`PC/SC error: ${err.message} (code: ${err.code})`);
305
+ } else {
306
+ throw err;
307
+ }
308
+ }
309
+ ```
310
+
311
+ ## Troubleshooting
312
+
313
+ ### "No readers available"
314
+ - Ensure a PC/SC compatible reader is connected
315
+ - On Linux, ensure `pcscd` service is running: `sudo systemctl status pcscd`
316
+
317
+ ### "PC/SC service not running"
318
+ - Linux: `sudo systemctl start pcscd`
319
+ - Windows: Check "Smart Card" service is running
320
+
321
+ ### "Sharing violation"
322
+ - Another application has exclusive access to the card
323
+ - Close other smart card applications
324
+
325
+ ### Build errors on Linux
326
+ - Install development headers: `sudo apt-get install libpcsclite-dev`
327
+
328
+ ## Migrating from v1.x
329
+
330
+ Version 2.0 is a complete rewrite using N-API for stability across Node.js versions.
331
+
332
+ ### Breaking Changes
333
+
334
+ | v1.x | v2.x |
335
+ |------|------|
336
+ | `device-activated` event | `reader-attached` event |
337
+ | `device-deactivated` event | `reader-detached` event |
338
+ | `event.device` | `reader` (passed directly) |
339
+ | `device.on('card-inserted')` | `devices.on('card-inserted')` |
340
+ | `card.issueCommand()` | `card.transmit()` |
341
+
342
+ ### Migration Example
343
+
344
+ **v1.x:**
345
+ ```javascript
346
+ const { Devices } = require('smartcard');
347
+ const devices = new Devices();
348
+
349
+ devices.on('device-activated', event => {
350
+ const device = event.device;
351
+ device.on('card-inserted', event => {
352
+ const card = event.card;
353
+ card.issueCommand(new CommandApdu({...}));
354
+ });
355
+ });
356
+ ```
357
+
358
+ **v2.x:**
359
+ ```javascript
360
+ const { Devices } = require('smartcard');
361
+ const devices = new Devices();
362
+
363
+ devices.on('reader-attached', reader => {
364
+ console.log('Reader:', reader.name);
365
+ });
366
+
367
+ devices.on('card-inserted', ({ reader, card }) => {
368
+ const response = await card.transmit([0x00, 0xA4, 0x04, 0x00]);
369
+ });
370
+
371
+ devices.start();
372
+ ```
373
+
374
+ ### Key Improvements in v2.x
375
+ - Works on Node.js 12, 14, 16, 18, 20, 22, 24+ without recompilation
376
+ - Native N-API bindings (no more NAN compatibility issues)
377
+ - Simpler flat event model
378
+ - Full TypeScript definitions
379
+ - Promise-based async API
380
+
381
+ ## License
382
+
383
+ MIT
384
+
385
+ ## Related Projects
386
+
387
+ - [nfc-pcsc](https://www.npmjs.com/package/nfc-pcsc) - NFC library built on smartcard
package/binding.gyp ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "targets": [{
3
+ "target_name": "smartcard_napi",
4
+ "cflags!": ["-fno-exceptions"],
5
+ "cflags_cc!": ["-fno-exceptions"],
6
+ "sources": [
7
+ "src/addon.cpp",
8
+ "src/pcsc_context.cpp",
9
+ "src/pcsc_reader.cpp",
10
+ "src/pcsc_card.cpp",
11
+ "src/async_workers.cpp",
12
+ "src/reader_monitor.cpp"
13
+ ],
14
+ "include_dirs": [
15
+ "<!@(node -p \"require('node-addon-api').include\")"
16
+ ],
17
+ "defines": [
18
+ "NAPI_VERSION=8",
19
+ "NAPI_CPP_EXCEPTIONS"
20
+ ],
21
+ "conditions": [
22
+ ["OS=='win'", {
23
+ "libraries": ["-lwinscard"],
24
+ "msvs_settings": {
25
+ "VCCLCompilerTool": {
26
+ "ExceptionHandling": 1
27
+ }
28
+ }
29
+ }],
30
+ ["OS=='mac'", {
31
+ "libraries": ["-framework PCSC"],
32
+ "xcode_settings": {
33
+ "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
34
+ "CLANG_CXX_LIBRARY": "libc++",
35
+ "MACOSX_DEPLOYMENT_TARGET": "10.15"
36
+ }
37
+ }],
38
+ ["OS=='linux'", {
39
+ "libraries": ["-lpcsclite"],
40
+ "include_dirs": ["/usr/include/PCSC"],
41
+ "cflags_cc": ["-fexceptions"]
42
+ }]
43
+ ]
44
+ }]
45
+ }
package/lib/devices.js ADDED
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+ const addon = require('../build/Release/smartcard_napi.node');
5
+
6
+ const { Context, ReaderMonitor } = addon;
7
+ const SCARD_STATE_PRESENT = addon.SCARD_STATE_PRESENT;
8
+ const SCARD_SHARE_SHARED = addon.SCARD_SHARE_SHARED;
9
+ const SCARD_PROTOCOL_T0 = addon.SCARD_PROTOCOL_T0;
10
+ const SCARD_PROTOCOL_T1 = addon.SCARD_PROTOCOL_T1;
11
+
12
+ /**
13
+ * High-level event-driven API for PC/SC devices
14
+ *
15
+ * Uses native ReaderMonitor for efficient background monitoring
16
+ * with ThreadSafeFunction to emit events from worker thread.
17
+ *
18
+ * Events:
19
+ * - 'reader-attached': Emitted when a reader is attached
20
+ * - 'reader-detached': Emitted when a reader is detached
21
+ * - 'card-inserted': Emitted when a card is inserted
22
+ * - 'card-removed': Emitted when a card is removed
23
+ * - 'error': Emitted on errors
24
+ */
25
+ class Devices extends EventEmitter {
26
+ constructor() {
27
+ super();
28
+ this._monitor = null;
29
+ this._context = null;
30
+ this._running = false;
31
+ this._readers = new Map(); // name -> { hasCard, card }
32
+ }
33
+
34
+ /**
35
+ * Start monitoring for device changes
36
+ */
37
+ start() {
38
+ if (this._running) {
39
+ return;
40
+ }
41
+
42
+ try {
43
+ // Create context for card connections
44
+ this._context = new Context();
45
+
46
+ // Create native monitor
47
+ this._monitor = new ReaderMonitor();
48
+ this._running = true;
49
+
50
+ // Start native monitoring with callback
51
+ this._monitor.start((event) => {
52
+ this._handleEvent(event);
53
+ });
54
+ } catch (err) {
55
+ this.emit('error', err);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Stop monitoring
61
+ */
62
+ stop() {
63
+ this._running = false;
64
+
65
+ if (this._monitor) {
66
+ try {
67
+ this._monitor.stop();
68
+ } catch (err) {
69
+ // Ignore stop errors
70
+ }
71
+ this._monitor = null;
72
+ }
73
+
74
+ // Disconnect any connected cards
75
+ for (const [name, state] of this._readers) {
76
+ if (state.card) {
77
+ try {
78
+ state.card.disconnect();
79
+ } catch (err) {
80
+ // Ignore disconnect errors
81
+ }
82
+ }
83
+ }
84
+ this._readers.clear();
85
+
86
+ if (this._context) {
87
+ try {
88
+ this._context.close();
89
+ } catch (err) {
90
+ // Ignore close errors
91
+ }
92
+ this._context = null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * List currently known readers
98
+ * @returns {Array} Array of reader names
99
+ */
100
+ listReaders() {
101
+ if (!this._context || !this._context.isValid) {
102
+ return [];
103
+ }
104
+ try {
105
+ return this._context.listReaders();
106
+ } catch (err) {
107
+ return [];
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Handle events from native monitor
113
+ */
114
+ async _handleEvent(event) {
115
+ if (!this._running) {
116
+ return;
117
+ }
118
+
119
+ const { type, reader: readerName, state, atr } = event;
120
+
121
+ switch (type) {
122
+ case 'reader-attached':
123
+ this._handleReaderAttached(readerName, state, atr);
124
+ break;
125
+
126
+ case 'reader-detached':
127
+ this._handleReaderDetached(readerName);
128
+ break;
129
+
130
+ case 'card-inserted':
131
+ await this._handleCardInserted(readerName, state, atr);
132
+ break;
133
+
134
+ case 'card-removed':
135
+ this._handleCardRemoved(readerName);
136
+ break;
137
+
138
+ case 'error':
139
+ // readerName contains error message for error events
140
+ this.emit('error', new Error(readerName));
141
+ break;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Handle reader attached
147
+ */
148
+ _handleReaderAttached(readerName, state, atr) {
149
+ // Initialize reader state
150
+ this._readers.set(readerName, {
151
+ hasCard: false,
152
+ card: null,
153
+ });
154
+
155
+ // Create a reader-like object for the event
156
+ const reader = {
157
+ name: readerName,
158
+ state: state,
159
+ atr: atr,
160
+ };
161
+
162
+ this.emit('reader-attached', reader);
163
+
164
+ // Check if card is already present
165
+ if ((state & SCARD_STATE_PRESENT) !== 0) {
166
+ this._handleCardInserted(readerName, state, atr);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Handle reader detached
172
+ */
173
+ _handleReaderDetached(readerName) {
174
+ const state = this._readers.get(readerName);
175
+
176
+ // If card was connected, emit card-removed first
177
+ if (state && state.hasCard) {
178
+ this._handleCardRemoved(readerName);
179
+ }
180
+
181
+ this._readers.delete(readerName);
182
+
183
+ const reader = { name: readerName };
184
+ this.emit('reader-detached', reader);
185
+ }
186
+
187
+ /**
188
+ * Handle card inserted
189
+ */
190
+ async _handleCardInserted(readerName, eventState, atr) {
191
+ let state = this._readers.get(readerName);
192
+ if (!state) {
193
+ state = { hasCard: false, card: null };
194
+ this._readers.set(readerName, state);
195
+ }
196
+
197
+ state.hasCard = true;
198
+
199
+ // Try to connect to the card
200
+ try {
201
+ const readers = this._context.listReaders();
202
+ const reader = readers.find(r => r.name === readerName);
203
+
204
+ if (reader) {
205
+ const card = await reader.connect(
206
+ SCARD_SHARE_SHARED,
207
+ SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1
208
+ );
209
+
210
+ state.card = card;
211
+
212
+ this.emit('card-inserted', {
213
+ reader: { name: readerName, state: eventState, atr: atr },
214
+ card: card,
215
+ });
216
+ }
217
+ } catch (err) {
218
+ // Emit error but don't fail
219
+ this.emit('error', err);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Handle card removed
225
+ */
226
+ _handleCardRemoved(readerName) {
227
+ const state = this._readers.get(readerName);
228
+ if (!state) {
229
+ return;
230
+ }
231
+
232
+ const card = state.card;
233
+ state.hasCard = false;
234
+ state.card = null;
235
+
236
+ if (card) {
237
+ try {
238
+ card.disconnect();
239
+ } catch (err) {
240
+ // Ignore - card is already removed
241
+ }
242
+ }
243
+
244
+ this.emit('card-removed', {
245
+ reader: { name: readerName },
246
+ card: card,
247
+ });
248
+ }
249
+ }
250
+
251
+ module.exports = { Devices };