smartcard 1.0.45 → 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 +1 -1
- package/README.md +387 -0
- package/binding.gyp +45 -0
- package/lib/devices.js +251 -0
- package/lib/errors.js +72 -0
- package/lib/index.d.ts +247 -0
- package/lib/index.js +76 -14
- package/package.json +56 -39
- package/src/addon.cpp +54 -0
- package/src/async_workers.cpp +234 -0
- package/src/async_workers.h +100 -0
- package/src/pcsc_card.cpp +248 -0
- package/src/pcsc_card.h +41 -0
- package/src/pcsc_context.cpp +224 -0
- package/src/pcsc_context.h +32 -0
- package/src/pcsc_errors.h +49 -0
- package/src/pcsc_reader.cpp +89 -0
- package/src/pcsc_reader.h +39 -0
- package/src/platform/pcsc.h +36 -0
- package/src/reader_monitor.cpp +344 -0
- package/src/reader_monitor.h +57 -0
- package/.prettierrc +0 -3
- package/README.MD +0 -371
- package/babel.config.json +0 -3
- package/demo/device-activated-promise.js +0 -12
- package/demo/device-activated.js +0 -12
- package/demo/device-deactivated-promise.js +0 -12
- package/demo/device-deactivated.js +0 -12
- package/demo/smartcard-demo.js +0 -100
- package/lib/Card.js +0 -129
- package/lib/CommandApdu.js +0 -109
- package/lib/Device.js +0 -138
- package/lib/Devices.js +0 -134
- package/lib/Iso7816Application.js +0 -169
- package/lib/ResponseApdu.js +0 -129
package/LICENSE
CHANGED
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 };
|