소스 검색

v1.2.0: add CLI stuff

tags/v1.2.0
AJ ONeal 6 달 전
부모
커밋
4ac6e7f8dd
5개의 변경된 파일601개의 추가작업 그리고 8개의 파일을 삭제
  1. 4
    2
      README.md
  2. 581
    0
      bin/keypairs.js
  3. 7
    3
      keypairs.js
  4. 1
    1
      package-lock.json
  5. 8
    2
      package.json

+ 4
- 2
README.md 파일 보기

@@ -11,13 +11,15 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
11 11
   * [x] Generate keypairs
12 12
     * [x] RSA
13 13
     * [x] ECDSA (P-256, P-384)
14
-  * [x] PEM-to-JWK
15
-  * [x] JWK-to-PEM
14
+  * [x] PEM-to-JWK (and SSH-to-JWK)
15
+  * [x] JWK-to-PEM (and JWK-to-SSH)
16 16
   * [x] Create JWTs (and sign JWS)
17 17
   * [x] SHA256 JWK Thumbprints
18 18
   * [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
19 19
     * [ ] OIDC
20 20
     * [ ] Auth0
21
+  * [ ] CLI
22
+    * See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)
21 23
 
22 24
 <!--
23 25
 

+ 581
- 0
bin/keypairs.js 파일 보기

@@ -0,0 +1,581 @@
1
+#!/usr/bin/env node
2
+'use strict';
3
+
4
+// I'm not proud of the way this code is written - it snowballed from a thought
5
+// experiment into a full-fledged CLI, literally overnight (as it it's 4:30am
6
+// right now), but I love what it accomplishes!
7
+
8
+/*global Promise*/
9
+var fs = require('fs');
10
+var Rasha = require('rasha');
11
+var Eckles = require('eckles');
12
+var Keypairs = require('../');
13
+var pkg = require('../package.json');
14
+
15
+var args = process.argv.slice(2);
16
+var opts = { jwks: [], jwts: [], jwss: [], payloads: [], names: [], filenames: [], files: [], pems: [] };
17
+var conflicts = {
18
+  'namedCurve': 'modulusLength'
19
+, 'public': 'private'
20
+};
21
+Object.keys(conflicts).forEach(function (k) {
22
+  conflicts[conflicts[k]] = k;
23
+});
24
+function set(key, val) {
25
+  if (opts[conflicts[key]]) {
26
+    console.error("cannot set '" + key + "' to '" + val + "': '" + conflicts[key] + "' already set as '" + opts[conflicts[key]] + "'");
27
+    process.exit(1);
28
+  }
29
+  if (opts[key]) {
30
+    console.error("cannot set '" + key + "' to '" + val + "': already set as '" + opts[key] + "'");
31
+    process.exit(1);
32
+  }
33
+  opts[key] = val;
34
+}
35
+
36
+// duck type all the things
37
+// TODO segment off by actions (gen, sign, verify) and allow parse/convert or gen before sign
38
+args.forEach(function (arg) {
39
+  var larg = arg.toLowerCase().replace(/[^\w]/g, '');
40
+  var narg = parseInt(arg, 10) || 0;
41
+  if (narg.toString() !== arg) {
42
+    // i.e. 2048.pem is a valid file name
43
+    narg = false;
44
+  }
45
+
46
+  if ('version' === arg) {
47
+    console.info(pkg.name, 'v' + pkg.version);
48
+    process.exit(0);
49
+  }
50
+
51
+  if (setTimes(arg)) {
52
+    return;
53
+  }
54
+  if (setIssuer(arg)) {
55
+    return;
56
+  }
57
+  if (setSubject(arg)) {
58
+    return;
59
+  }
60
+
61
+  if ('ecdsa' === larg || 'ec' === larg) {
62
+    set('kty', "EC");
63
+    if (opts.modulusLength) {
64
+      console.error("EC keys do not have bit lengths such as '" + opts.modulusLength + "'. Choose either the P-256 or P-384 'curve' instead.");
65
+      process.exit(1);
66
+    }
67
+  }
68
+  if ('rsa' === larg) {
69
+    set('kty', "RSA");
70
+    if (opts.namedCurve) {
71
+      console.error("RSA keys do not have curves such as '" + opts.namedCurve + "'. Choose a modulus bit length, such as 2048 instead.");
72
+      process.exit(1);
73
+    }
74
+    return;
75
+  }
76
+
77
+  // P-384
78
+  if (-1 !== ['256', 'p256', 'prime256v1', 'secp256r1'].indexOf(larg)) {
79
+    set('namedCurve', "P-256");
80
+    return;
81
+  }
82
+
83
+  // P-384
84
+  if (-1 !== ['384', 'p384', 'secp384r1'].indexOf(larg)) {
85
+    set('namedCurve', "P-384");
86
+    return;
87
+  }
88
+
89
+  // RSA Modulus Length
90
+  if (narg) {
91
+    if (narg < 2048 || narg % 8 || narg > 8192) {
92
+      console.error("RSA modulusLength must be >=2048, <=8192 and divisible by 8");
93
+      process.exit(1);
94
+    }
95
+    set('modulusLength', narg);
96
+    return;
97
+  }
98
+
99
+  // Booleans
100
+  if (-1 !== [ 'private', 'public', 'nocompact', 'nofetch', 'debug', 'overwrite' ].indexOf(arg)) {
101
+    console.log(arg);
102
+    set(arg, true);
103
+    return;
104
+  }
105
+  if ('uncompressed' === arg) {
106
+    set('uncompressed', true);
107
+    return;
108
+  }
109
+  if (-1 !== [ 'gen', 'sign', 'verify', 'decode' ].indexOf(arg)) {
110
+    set('action', arg);
111
+    return;
112
+  }
113
+
114
+  // Key format and encoding
115
+  if (-1 !== [ 'spki', 'pkix' ].indexOf(larg)) {
116
+    set('pubFormat', 'spki');
117
+    return;
118
+  }
119
+  // TODO add ssh private key support (it's already built in jwk-to-ssh)
120
+  if ('ssh' === larg) {
121
+    set('pubFormat', 'ssh');
122
+    return;
123
+  }
124
+  if (-1 !== [ 'openssh', 'sec1', 'pkcs1', 'pkcs8' ].indexOf(larg)) {
125
+    // pkcs1 can be public or private, it's ambiguous
126
+    if (!opts.privFormat) {
127
+      set('privFormat', larg);
128
+      return;
129
+    }
130
+
131
+    if ('pkcs1' === larg || 'ssh' === larg) {
132
+      set('pubFormat', larg);
133
+      return;
134
+    }
135
+    if ('openssh' === larg) {
136
+      console.warn("specifying 'openssh' twice? ...assuming that you meant 'ssh'");
137
+      set('pubFormat', 'ssh');
138
+      return;
139
+    }
140
+    if ('pkcs8' === larg) {
141
+      console.warn("specifying 'pkcs8' twice? ...assuming that you meant 'spki' (pkix)");
142
+      set('pubFormat', 'spki');
143
+      return;
144
+    }
145
+    if ('sec1' === larg) {
146
+      console.warn("specifying 'sec1' twice? ...assuming that you meant 'spki' (pkix)");
147
+      set('pubFormat', 'spki');
148
+      return;
149
+    }
150
+    return;
151
+  }
152
+  if ('jwk' === larg) {
153
+    if (!opts.privFormat) {
154
+      set('privFormat', larg);
155
+    } else {
156
+      set('pubFormat', larg);
157
+    }
158
+    return;
159
+  }
160
+  if ('pem' === larg || 'der' === larg || 'json' === larg) {
161
+    if (!opts.privEncoding) {
162
+      set('privEncoding', larg);
163
+    } else {
164
+      set('pubEncoding', larg);
165
+    }
166
+    return;
167
+  }
168
+
169
+  // Filename
170
+  try {
171
+    fs.accessSync(arg);
172
+    opts.filenames.push(arg);
173
+    opts.names.push({ taken: true, name: arg });
174
+    if (!guessFile(arg)) {
175
+      opts.files.push(arg);
176
+    }
177
+    return;
178
+  } catch(e) { /* not keypath */ }
179
+
180
+  // Test for JWK-ness / payload-ness
181
+  if (guess(arg)) {
182
+    return;
183
+  }
184
+
185
+  // Test for JWT-ness
186
+  if (setJwt(arg)) {
187
+    return;
188
+  }
189
+
190
+  // Possibly the output file
191
+  if (!opts.extra1) {
192
+    opts.extra1 = arg;
193
+    opts.names.push({ taken: false, name: arg });
194
+    return;
195
+  }
196
+  if (!opts.extra2) {
197
+    opts.extra2 = arg;
198
+    opts.names.push({ taken: false, name: arg });
199
+    return;
200
+  }
201
+  // check if it's a valid output key
202
+
203
+  console.error("too many arguments or didn't understand argument '" + arg + "'");
204
+  if (opts.debug) {
205
+    console.warn(opts);
206
+  }
207
+  process.exit(1);
208
+});
209
+
210
+function guessFile(filename) {
211
+  try {
212
+    // TODO der support
213
+    var txt = fs.readFileSync(filename).toString('utf8');
214
+    return guess(txt, filename);
215
+  } catch(e) {
216
+    return false;
217
+  }
218
+}
219
+
220
+function guess(txt, filename) {
221
+  try {
222
+    var json = JSON.parse(txt);
223
+    if (-1 !== [ 'RSA', 'EC' ].indexOf(json.kty)) {
224
+      opts.jwks.push({ jwk: json, filename: filename });
225
+      return true;
226
+    } else if (json.signature && json.payload && (json.header || json.protected)) {
227
+      opts.jwss.push(json);
228
+      return true;
229
+    } else {
230
+      opts.payloads.push(txt);
231
+      return true;
232
+    }
233
+  } catch(e) {
234
+    try {
235
+      var pem = Eckles.importSync({ pem: txt });
236
+      // pem._string = txt;
237
+      opts.pems.push(pem);
238
+      return true;
239
+    } catch(e) {
240
+      try {
241
+        var pem = Rasha.importSync({ pem: txt });
242
+        // pem._string = txt;
243
+        opts.pems.push(pem);
244
+        return true;
245
+      } catch(e) {
246
+        // ignore
247
+      }
248
+    }
249
+  }
250
+  return false;
251
+}
252
+
253
+// node bin/keypairs.js debug spki pem json pkcs1 ~/.ssh/id_rsa.pub foo.pem bar.pem 'abc.abc.abc' '{"kty":"EC"}' '{}' '{"signature":"x", "payload":"x", "header":"x"}' '{"signature":"x", "payload":"x", "protected":"x"}' verify
254
+if (opts.debug) {
255
+  console.warn(opts);
256
+}
257
+
258
+var kp;
259
+
260
+if ('gen' === opts.action || (!opts.action && !opts.names.length)) {
261
+  if (opts.names.length > 2) {
262
+    console.error("there should only be two output files at most when generating keypairs");
263
+    console.error(opts.names.map(function (t) { return t.name; }));
264
+    process.exit(1);
265
+    return;
266
+  }
267
+
268
+  kp = genKeypair();
269
+} else if ('decode' === opts.action) {
270
+  if (!opts.jwts.length) {
271
+    console.error("no JWTs specified to decode");
272
+    process.exit(1);
273
+    return;
274
+  }
275
+
276
+  return Promise.all(opts.jwts.map(function (jwt, i) {
277
+    try {
278
+      var decoded = decodeJwt(jwt);
279
+      console.info("Decoded #" + (i + 1) + ":");
280
+      console.info(JSON.stringify(decoded, null, 2));
281
+    } catch(e) {
282
+      console.error("Failed to decode #" + (i + 1) + ":");
283
+      console.error(e);
284
+    }
285
+  }));
286
+} else if ('verify' === opts.action || (!opts.action && opts.jwts.length)) {
287
+  if (!opts.jwts.length) {
288
+    console.error("no JWTs specified to verify");
289
+    process.exit(1);
290
+    return;
291
+  }
292
+
293
+  return Promise.all(opts.jwts.map(function (jwt, i) {
294
+    return require('keyfetch').verify({ jwt: jwt }).then(function (decoded) {
295
+      console.info("Verified #" + (i + 1) + ":");
296
+      console.info(JSON.stringify(decoded, null, 2));
297
+    }).catch(function (err) {
298
+      console.error("Failed to verify #" + (i + 1) + ":");
299
+      console.error(err);
300
+    });
301
+  }));
302
+} else {
303
+  if (opts.names.length > 3) {
304
+    console.error("there should only be one input file and up to two output files when converting keypairs");
305
+    console.error(opts.names.map(function (t) { return t.name; }));
306
+    process.exit(1);
307
+    return;
308
+  }
309
+  kp = Promise.resolve(readKeypair());
310
+}
311
+
312
+if ('sign' === opts.action) {
313
+  return kp.then(function (pair) {
314
+    var jwk = pair.private;
315
+    if (!jwk || !jwk.d) {
316
+      console.error("the first key was not a private key");
317
+      console.error(opts.names.map(function (t) { return t.name; }));
318
+      process.exit(1);
319
+      return;
320
+    }
321
+    if (!opts.payloads.length) {
322
+      opts.payloads.push('{}');
323
+    }
324
+    return Promise.all(opts.payloads.map(function (payload) {
325
+      var claims = JSON.parse(payload);
326
+      if (!claims.iss) { claims.iss = opts.issuer; }
327
+      if (!claims.iss) { console.warn("No issuer given, token will not be verifiable"); }
328
+      if (!claims.sub) { claims.sub = opts.sub; }
329
+      if (!claims.exp) {
330
+        if (!opts.expiresAt) { setTimes('15m'); }
331
+        claims.exp = opts.expiresAt;
332
+      }
333
+      if (!claims.iat) { claims.iat = opts.issuedAt; }
334
+      if (!claims.nbf) { claims.nbf = opts.nbf; }
335
+      return Keypairs.signJwt({ jwk: pair.private, claims: claims }).then(function (jwt) {
336
+        console.info(jwt);
337
+      });
338
+    }));
339
+  });
340
+} else {
341
+  return convertKeypair();
342
+}
343
+
344
+function readKeypair() {
345
+  // note that the jwk may be a string
346
+  var jwkopts = opts.jwks.shift();
347
+  var jwk = jwkopts && jwkopts.jwk;
348
+  if (!jwk) {
349
+    console.error("no keys could be parsed from the given arguments");
350
+    console.error(opts.names.map(function (t) { return t.name; }));
351
+    process.exit(1);
352
+    return;
353
+  }
354
+
355
+  // omit the primary private key from the list of actual (or soon-to-be) files
356
+  if (jwkopts.filename) {
357
+    opts.names = opts.names.filter(function (name) {
358
+      console.log(jwkopts.filename, name.name);
359
+      return name.name !== jwkopts.filename;
360
+    });
361
+  }
362
+
363
+  var pair = { private: null, public: null };
364
+  if (jwk.d) {
365
+    pair.private = jwk;
366
+  }
367
+  pair.public = Keypairs._neuter({ jwk: jwk });
368
+  return pair;
369
+}
370
+
371
+function convertKeypair() {
372
+  var pair = readKeypair();
373
+
374
+  var ps = [];
375
+  if (pair.private && !opts.public) {
376
+    if ((!opts.privEncoding || 'json' === opts.privEncoding) && (!opts.privFormat || 'jwk' === opts.privFormat)) {
377
+      ps.push(Promise.resolve(pair.private));
378
+    } else {
379
+      ps.push(Keypairs.export({ jwk: pair.private, format: opts.privFormat, encoding: opts.privEncoding }));
380
+    }
381
+  }
382
+  if (!opts.private) {
383
+    if (opts.public) {
384
+      if (!opts.pubFormat) { opts.pubFormat = opts.privFormat; }
385
+      if (!opts.pubEncoding) { opts.pubEncoding = opts.privEncoding; }
386
+    }
387
+
388
+    if ((!opts.pubEncoding || 'json' === opts.pubEncoding) && (!opts.pubFormat || 'jwk' === opts.pubFormat)) {
389
+      ps.push(Promise.resolve(pair.public));
390
+    } else {
391
+      ps.push(Keypairs.export({ jwk: pair.public, format: opts.pubFormat, encoding: opts.pubEncoding, public: true }));
392
+    }
393
+  }
394
+  return Promise.all(ps).then(function (arr) {
395
+    // only use the first key
396
+    var key = convert(0, opts.public);
397
+    if (opts.public) {
398
+      // end early
399
+      if (opts.names.length) {
400
+        writeFile(opts.names[0].name, key, !opts.public); // todo make pub/priv param consistent, not flip-flop
401
+      } else {
402
+      console.warn(key + "\n");
403
+      }
404
+      return;
405
+    }
406
+
407
+    // private key stuff
408
+    if (opts.names.length) {
409
+      writeFile(opts.names[0].name, key, true);
410
+    } else {
411
+      console.info(key + "\n");
412
+    }
413
+
414
+    // pub key stuff
415
+    if (!opts.private) {
416
+      if (opts.names.length >= 2) {
417
+        writeFile(opts.names[1].name, key, false);
418
+      } else {
419
+        console.warn(key + "\n");
420
+      }
421
+    }
422
+
423
+    return pair;
424
+
425
+    function convert(i, pub) {
426
+      if (arr[i].kty) {
427
+        if (pub) {
428
+          if (opts.expiresAt) { arr[i].exp = opts.expiresAt; }
429
+          arr[i].use = "sig";
430
+        }
431
+        arr[i] = JSON.stringify(arr[i]);
432
+      }
433
+      return arr[i];
434
+    }
435
+  });
436
+}
437
+
438
+function genKeypair() {
439
+  return Keypairs.generate({
440
+    kty: opts.kty
441
+  , modulusLength: opts.modulusLength
442
+  , namedCurve: opts.namedCurve
443
+  }).then(function (pair) {
444
+    var ps = [];
445
+    if ((!opts.privEncoding || 'json' === opts.privEncoding) && (!opts.privFormat || 'jwk' === opts.privFormat)) {
446
+      ps.push(Promise.resolve(pair.private));
447
+    } else {
448
+      ps.push(Keypairs.export({ jwk: pair.private, format: opts.privFormat, encoding: opts.privEncoding }));
449
+    }
450
+    if ((!opts.pubEncoding || 'json' === opts.pubEncoding) && (!opts.pubFormat || 'jwk' === opts.pubFormat)) {
451
+      ps.push(Promise.resolve(pair.public));
452
+    } else {
453
+      ps.push(Keypairs.export({ jwk: pair.public, format: opts.pubFormat, encoding: opts.pubEncoding, public: true }));
454
+    }
455
+    return Promise.all(ps).then(function (arr) {
456
+      if (arr[0].kty) {
457
+        arr[0] = JSON.stringify(arr[0]);
458
+      }
459
+      if (arr[1].kty) {
460
+        if (opts.expiresAt) { arr[1].exp = opts.expiresAt; }
461
+        arr[1].use = "sig";
462
+        arr[1] = JSON.stringify(arr[1]);
463
+      }
464
+      if (!opts.names.length) {
465
+        console.info(arr[0] + "\n");
466
+        console.warn(arr[1] + "\n");
467
+      }
468
+      if (opts.names.length >= 1) {
469
+        writeFile(opts.names[0].name, arr[0], true);
470
+        if (!opts.private && opts.names.length >= 2) {
471
+          writeFile(opts.names[1].name, arr[1]);
472
+        }
473
+      }
474
+
475
+      return pair;
476
+    });
477
+  });
478
+}
479
+
480
+function writeFile(name, key, priv) {
481
+  var overwrite;
482
+  try {
483
+    fs.accessSync(name);
484
+    overwrite = opts.overwrite;
485
+    if (!opts.overwrite) {
486
+      if (priv) {
487
+        console.info(key + "\n");
488
+      } else {
489
+        console.warn(key + "\n");
490
+      }
491
+      console.error("'" + name + "' exists! force overwrite with 'overwrite'");
492
+      process.exit(1);
493
+      return;
494
+    }
495
+  } catch(e) {
496
+    // the file does not exist (or cannot be accessed)
497
+  }
498
+  fs.writeFileSync(name, key);
499
+  if (overwrite) {
500
+    console.info("Overwrote " + (priv ? "private" : "public") + " key at '" + name + "'");
501
+  } else {
502
+    console.info("Wrote " + (priv ? "private" : "public") + " key to '" + name + "'");
503
+  }
504
+}
505
+
506
+function setJwt(arg) {
507
+  try {
508
+    var jwt = arg.match(/^([\w-]+)\.([\w-]+)\.([\w-]+)$/);
509
+    // make sure header is a JWT header
510
+    JSON.parse(Buffer.from(jwt[1], 'base64'));
511
+    opts.jwts.push(arg);
512
+    return true;
513
+  } catch(e) {
514
+    // ignore
515
+  }
516
+}
517
+
518
+function setSubject(arg) {
519
+  if (!/.+@[a-z0-9_-]+\.[a-z0-9_-]+/i.test(arg)) {
520
+    return false;
521
+  }
522
+
523
+  opts.subject = arg;
524
+  return false;
525
+}
526
+
527
+function setIssuer(arg) {
528
+  if (!/^https?:\/\/[a-z0-9_-]+\.[a-z0-9_-]+/i.test(arg)) {
529
+    return false;
530
+  }
531
+
532
+  try {
533
+    new URL(arg);
534
+    opts.issuer = arg.replace(/\/$/, '');
535
+    return true;
536
+  } catch(e) {
537
+  }
538
+  return false;
539
+}
540
+
541
+function setTimes(arg) {
542
+  var t = arg.match(/^(\-?\d+)([dhms])$/i);
543
+  if (!t || !t[0]) {
544
+    return false;
545
+  }
546
+
547
+  var num = parseInt(t[1], 10);
548
+  var unit = t[2];
549
+  var mult = 1;
550
+  opts.issuedAt = Math.round(Date.now()/1000);
551
+  switch(unit) {
552
+    // fancy fallthrough, what fun!
553
+    case 'd':
554
+      mult *= 24;
555
+      /*falls through*/
556
+    case 'h':
557
+      mult *= 60;
558
+      /*falls through*/
559
+    case 'm':
560
+      mult *= 60;
561
+      /*falls through*/
562
+    case 's':
563
+      mult *= 1;
564
+  }
565
+  if (!opts.expiresIn) {
566
+    opts.expiresIn = mult * num;
567
+    opts.expiresAt = opts.issuedAt + opts.expiresIn;
568
+  } else {
569
+    opts.nbf = opts.issuedAt + (mult * num);
570
+  }
571
+  return true;
572
+}
573
+
574
+function decodeJwt(jwt) {
575
+  var parts = jwt.split('.');
576
+  return {
577
+    header: JSON.parse(Buffer.from(parts[0], 'base64'))
578
+  , payload: JSON.parse(Buffer.from(parts[1], 'base64'))
579
+  , signature: parts[2] //Buffer.from(parts[2], 'base64')
580
+  };
581
+}

+ 7
- 3
keypairs.js 파일 보기

@@ -87,9 +87,7 @@ Keypairs.export = function (opts) {
87 87
   });
88 88
 };
89 89
 
90
-Keypairs.publish = function (opts) {
91
-  if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }
92
-
90
+Keypairs._neuter = function (opts) {
93 91
   // trying to find the best balance of an immutable copy with custom attributes
94 92
   var jwk = {};
95 93
   Object.keys(opts.jwk).forEach(function (k) {
@@ -97,6 +95,12 @@ Keypairs.publish = function (opts) {
97 95
     if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; }
98 96
     jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
99 97
   });
98
+  return jwk;
99
+};
100
+Keypairs.publish = function (opts) {
101
+  if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); }
102
+
103
+  var jwk = Keypairs._neuter(opts);
100 104
 
101 105
   if (!jwk.exp) {
102 106
     if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; }

+ 1
- 1
package-lock.json 파일 보기

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "keypairs",
3
-  "version": "1.1.0",
3
+  "version": "1.2.0",
4 4
   "lockfileVersion": 1,
5 5
   "requires": true,
6 6
   "dependencies": {

+ 8
- 2
package.json 파일 보기

@@ -1,12 +1,18 @@
1 1
 {
2 2
   "name": "keypairs",
3
-  "version": "1.1.0",
3
+  "version": "1.2.0",
4 4
   "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
5 5
   "main": "keypairs.js",
6
-  "files": [],
6
+  "files": [
7
+    "CLI.md",
8
+    "bin/keypairs.js"
9
+  ],
7 10
   "scripts": {
8 11
     "test": "node test.js"
9 12
   },
13
+  "bin": {
14
+    "keypairs": "bin/keypairs.js"
15
+  },
10 16
   "repository": {
11 17
     "type": "git",
12 18
     "url": "https://git.coolaj86.com/coolaj86/keypairs.js"

Loading…
취소
저장