v3.1.0: support `zones` test, made prettier
This commit is contained in:
parent
32030b9d80
commit
7b079bcf3a
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
74
README.md
74
README.md
|
@ -11,15 +11,15 @@ and [ACME.js](https://git.rootprojects.org/root/acme-v2.js).
|
||||||
_acme-challenge.example.com TXT xxxxxxxxxxxxxxxx TTL 60
|
_acme-challenge.example.com TXT xxxxxxxxxxxxxxxx TTL 60
|
||||||
```
|
```
|
||||||
|
|
||||||
* Prints the ACME challenge DNS Host and DNS Key Authorization Digest to the terminal
|
- Prints the ACME challenge DNS Host and DNS Key Authorization Digest to the terminal
|
||||||
* (waits for you to hit enter before continuing)
|
- (waits for you to hit enter before continuing)
|
||||||
* Let's you know when the challenge as succeeded or failed, and is safe to remove.
|
- Let's you know when the challenge as succeeded or failed, and is safe to remove.
|
||||||
|
|
||||||
Other ACME Challenge Reference Implementations:
|
Other ACME Challenge Reference Implementations:
|
||||||
|
|
||||||
* [acme-http-01-cli](https://git.rootprojects.org/root/acme-http-01-cli.js.git)
|
- [acme-http-01-cli](https://git.rootprojects.org/root/acme-http-01-cli.js.git)
|
||||||
* [acme-http-01-fs](https://git.rootprojects.org/root/acme-http-01-webroot.js.git)
|
- [acme-http-01-fs](https://git.rootprojects.org/root/acme-http-01-webroot.js.git)
|
||||||
* [**acme-dns-01-cli**](https://git.rootprojects.org/root/acme-dns-01-cli.js.git)
|
- [**acme-dns-01-cli**](https://git.rootprojects.org/root/acme-dns-01-cli.js.git)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
@ -31,16 +31,16 @@ If you have `greenlock@v2.6` or lower, you'll need the old `le-challenge-dns@2.x
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```js
|
||||||
var Greenlock = require('greenlock');
|
var Greenlock = require('greenlock');
|
||||||
|
|
||||||
Greenlock.create({
|
Greenlock.create({
|
||||||
...
|
challenges: {
|
||||||
, challenges: { 'http-01': require('acme-http-01-fs')
|
'http-01': require('acme-http-01-fs'),
|
||||||
, 'dns-01': require('acme-dns-01-cli').create({ debug: true })
|
'dns-01': require('acme-dns-01-cli').create({ debug: true }),
|
||||||
, 'tls-alpn-01': require('acme-tls-alpn-01-cli')
|
'tls-alpn-01': require('acme-tls-alpn-01-cli')
|
||||||
}
|
}
|
||||||
...
|
// ...
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -49,11 +49,9 @@ overwriting the default with the one that you want in `approveDomains()`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function approveDomains(opts) {
|
function approveDomains(opts) {
|
||||||
...
|
// ...
|
||||||
|
|
||||||
if (!opts.challenges) { opts.challenges = {}; }
|
if (!opts.challenges) { opts.challenges = {}; }
|
||||||
opts.challenges['dns-01'] = acmeDns01Cli;
|
opts.challenges['dns-01'] = acmeDns01Cli;
|
||||||
opts.challenges['http-01'] = ...
|
|
||||||
|
|
||||||
return Promise.resolve({ ... });
|
return Promise.resolve({ ... });
|
||||||
}
|
}
|
||||||
|
@ -66,36 +64,37 @@ it will require 6 individual challenges.
|
||||||
|
|
||||||
For ACME Challenge:
|
For ACME Challenge:
|
||||||
|
|
||||||
* `set(opts)`
|
- `set(opts)`
|
||||||
* `remove(opts)`
|
- `remove(opts)`
|
||||||
|
|
||||||
The `dns-01` strategy supports wildcards (whereas `http-01` does not).
|
The `dns-01` strategy supports wildcards (whereas `http-01` does not).
|
||||||
|
|
||||||
The options object has whatever options were set in `approveDomains()`
|
The options object has whatever options were set in `approveDomains()`
|
||||||
as well as the `challenge`, which looks like this:
|
as well as the `challenge`, which looks like this:
|
||||||
|
|
||||||
```js
|
```json
|
||||||
{ challenge: {
|
{
|
||||||
identifier: { type: 'dns', value: 'example.com'
|
"challenge": {
|
||||||
, wildcard: true
|
"identifier": { "type": "dns", "value": "example.com" },
|
||||||
, altname: '*.example.com'
|
"wildcard": true,
|
||||||
, type: 'dns-01'
|
"altname": "*.example.com",
|
||||||
, token: 'xxxxxx'
|
"type": "dns-01",
|
||||||
, keyAuthorization: 'xxxxxx.abc123'
|
"token": "xxxxxx",
|
||||||
, dnsHost: '_acme-challenge.example.com'
|
"keyAuthorization": "xxxxxx.abc123",
|
||||||
, dnsAuthorization: 'xyz567'
|
"dnsHost": "_acme-challenge.example.com",
|
||||||
, expires: '1970-01-01T00:00:00Z'
|
"dnsAuthorization": "xyz567",
|
||||||
|
"expires": "1970-01-01T00:00:00Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For greenlock.js internals:
|
For greenlock.js internals:
|
||||||
|
|
||||||
* `options` stores the internal defaults merged with the user-supplied options
|
- `options` stores the internal defaults merged with the user-supplied options
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
|
||||||
* `get(limitedOpts)`
|
- `get(limitedOpts)`
|
||||||
|
|
||||||
Note: Typically there wouldn't be a `get()` for DNS because the NameServer (not Greenlock) answers the requests.
|
Note: Typically there wouldn't be a `get()` for DNS because the NameServer (not Greenlock) answers the requests.
|
||||||
It could be used for testing implementations, but that's about it.
|
It could be used for testing implementations, but that's about it.
|
||||||
|
@ -104,12 +103,13 @@ It could be used for testing implementations, but that's about it.
|
||||||
If there were an implementation of Greenlock integrated directly into
|
If there were an implementation of Greenlock integrated directly into
|
||||||
a NameServer (which currently there is not), it would probably look like this:
|
a NameServer (which currently there is not), it would probably look like this:
|
||||||
|
|
||||||
```js
|
```json
|
||||||
{ challenge: {
|
{
|
||||||
type: 'dns-01'
|
"challenge": {
|
||||||
, identifier: { type: 'dns', value: 'example.com' }
|
"type": "dns-01",
|
||||||
, token: 'abc123'
|
"identifier": { "type": "dns", "value": "example.com" },
|
||||||
, dnsHost: '_acme-challenge.example.com'
|
"token": "abc123",
|
||||||
|
"dnsHost": "_acme-challenge.example.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
184
index.js
184
index.js
|
@ -1,11 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
/*global Promise*/
|
/*global Promise*/
|
||||||
|
|
||||||
var Challenge = module.exports;
|
var Challenge = module.exports;
|
||||||
|
|
||||||
// If your implementation needs config options, set them. Otherwise, don't bother (duh).
|
// If your implementation needs config options, set them. Otherwise, don't bother (duh).
|
||||||
Challenge.create = function (config) {
|
Challenge.create = function(config) {
|
||||||
|
|
||||||
var challenger = {};
|
var challenger = {};
|
||||||
|
|
||||||
// Note: normally you'd these right in the method body, but for the sake of
|
// Note: normally you'd these right in the method body, but for the sake of
|
||||||
|
@ -14,19 +12,24 @@ Challenge.create = function (config) {
|
||||||
// Note: All of these methods can be synchronous, async, Promise, and callback-style
|
// Note: All of these methods can be synchronous, async, Promise, and callback-style
|
||||||
// (the calling functions check function.length and then Promisify accordingly)
|
// (the calling functions check function.length and then Promisify accordingly)
|
||||||
|
|
||||||
// Called when it's tiem to set the challenge
|
// Fetches an array of zone name strings
|
||||||
challenger.set = function (opts, cb) {
|
challenger.zones = function(opts, cb) {
|
||||||
|
return Challenge._getZones(opts, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Called when it's time to set the challenge
|
||||||
|
challenger.set = function(opts, cb) {
|
||||||
return Challenge._setDns(opts, cb);
|
return Challenge._setDns(opts, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Called when it's time to remove the challenge
|
// Called when it's time to remove the challenge
|
||||||
challenger.remove = function (opts) {
|
challenger.remove = function(opts, cb) {
|
||||||
return Challenge._removeDns(opts);
|
return Challenge._removeDns(opts, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optional (only really useful for http and testing)
|
// Optional (only really useful for http and testing)
|
||||||
// Called when the challenge needs to be retrieved
|
// Called when the challenge needs to be retrieved
|
||||||
challenger.get = function (opts) {
|
challenger.get = function(opts) {
|
||||||
return Challenge._getDns(opts);
|
return Challenge._getDns(opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,88 +41,167 @@ Challenge.create = function (config) {
|
||||||
return challenger;
|
return challenger;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show the user the token and key and wait for them to be ready to continue
|
||||||
|
Challenge._getZones = function(args, cb) {
|
||||||
|
// if you need per-run / per-domain options set them in approveDomains() and they'll be on 'args' here.
|
||||||
|
if (!Array.isArray(args.dnsHosts)) {
|
||||||
|
console.error(
|
||||||
|
'You must be using Greenlock v2.7+ to use acme-dns-01-cli v3+'
|
||||||
|
);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
console.info();
|
||||||
|
console.info('############################');
|
||||||
|
console.info('# Step 1: Get Domain Zones #');
|
||||||
|
console.info('############################');
|
||||||
|
console.info();
|
||||||
|
console.info(
|
||||||
|
'Enter a comma or space delimited list of domain zones to which the following domain records belong.'
|
||||||
|
);
|
||||||
|
console.info();
|
||||||
|
console.info(
|
||||||
|
'Example:' +
|
||||||
|
'\n\tDOMAIN RECORD\t=>\tDOMAIN ZONE' +
|
||||||
|
'\n\texample.com\t=>\texample.com' +
|
||||||
|
'\n\tfoo.example.com\t=>\texample.com' +
|
||||||
|
'\n\tbar.example.com\t=>\texample.com' +
|
||||||
|
'\n\texample.co.uk\t=>\texample.co.uk' +
|
||||||
|
'\n\nYou would enter: example.com, example.co.uk'
|
||||||
|
);
|
||||||
|
console.info();
|
||||||
|
console.info('Domain RECORDS: ', args.dnsHosts.join(', '));
|
||||||
|
process.stdout.write('Domain ZONE list: ');
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.once('data', function(chunk) {
|
||||||
|
process.stdin.pause();
|
||||||
|
var zones = chunk
|
||||||
|
.toString('utf8')
|
||||||
|
.trim()
|
||||||
|
.split(/[,\s]+/);
|
||||||
|
console.info('Got Domain Zones:', zones);
|
||||||
|
setTimeout(function() {
|
||||||
|
cb(null, zones);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Show the user the token and key and wait for them to be ready to continue
|
// Show the user the token and key and wait for them to be ready to continue
|
||||||
Challenge._setDns = function (args, cb) {
|
Challenge._setDns = function(args, cb) {
|
||||||
// if you need per-run / per-domain options set them in approveDomains() and they'll be on 'args' here.
|
// if you need per-run / per-domain options set them in approveDomains() and they'll be on 'args' here.
|
||||||
if (!args.challenge) {
|
if (!args.challenge) {
|
||||||
console.error("You must be using Greenlock v2.7+ to use greenlock-challenge-dns v3+");
|
console.error(
|
||||||
|
'You must be using Greenlock v2.7+ to use acme-dns-01-cli v3+'
|
||||||
|
);
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
var ch = args.challenge;
|
var ch = args.challenge;
|
||||||
|
|
||||||
console.info("");
|
console.info('\n\n\n\n\n');
|
||||||
|
console.info('#################################');
|
||||||
|
console.info('# Step 2: Set Domain TXT Record #');
|
||||||
|
console.info('#################################');
|
||||||
|
console.info('');
|
||||||
console.info("[ACME dns-01 '" + ch.altname + "' CHALLENGE]");
|
console.info("[ACME dns-01 '" + ch.altname + "' CHALLENGE]");
|
||||||
console.info("You're about to receive the following DNS query:");
|
console.info("You're about to receive the following DNS query:");
|
||||||
console.info("");
|
console.info('');
|
||||||
console.info("\tTXT\t" + ch.dnsHost + "\t" + ch.dnsAuthorization + "\tTTL 60");
|
console.info(
|
||||||
console.info("");
|
'\tTXT\t' + ch.dnsHost + '\t' + ch.dnsAuthorization + '\tTTL 60'
|
||||||
|
);
|
||||||
|
console.info('');
|
||||||
if (ch.debug) {
|
if (ch.debug) {
|
||||||
console.info("Debug Info:");
|
console.info('Debug Info:');
|
||||||
console.info("");
|
console.info('');
|
||||||
console.info(JSON.stringify(dnsChallengeToJson(ch), null, ' ').replace(/^/gm, '\t'));
|
console.info(
|
||||||
console.info("");
|
JSON.stringify(dnsChallengeToJson(ch), null, ' ').replace(/^/gm, '\t')
|
||||||
|
);
|
||||||
|
console.info('');
|
||||||
}
|
}
|
||||||
console.info("Go set that DNS record, wait a few seconds for it to propagate, and then continue when ready");
|
console.info(
|
||||||
console.info("[Press the ANY key to continue...]");
|
'Go set that DNS record, wait a few seconds for it to propagate, and then continue when ready'
|
||||||
|
);
|
||||||
|
console.info('[Press the ANY key to continue...]');
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
process.stdin.once('data', function () {
|
process.stdin.once('data', function() {
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
cb(null, null);
|
setTimeout(function() {
|
||||||
|
cb(null, null);
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// might as well tell the user that whatever they were setting up has been checked
|
// might as well tell the user that whatever they were setting up has been checked
|
||||||
Challenge._removeDns = function (args) {
|
Challenge._removeDns = function(args, cb) {
|
||||||
var ch = args.challenge;
|
var ch = args.challenge;
|
||||||
console.info("");
|
console.info('\n\n\n\n\n');
|
||||||
|
console.info('####################################');
|
||||||
|
console.info('# Step 4: Remove Domain TXT Record #');
|
||||||
|
console.info('####################################');
|
||||||
|
console.info('');
|
||||||
console.info("[ACME dns-01 '" + ch.altname + "' COMPLETE]: " + ch.status);
|
console.info("[ACME dns-01 '" + ch.altname + "' COMPLETE]: " + ch.status);
|
||||||
console.info("Challenge complete. You may now remove the DNS-01 challenge record:");
|
console.info(
|
||||||
console.info("");
|
'Challenge complete. You may now remove the DNS-01 challenge record:'
|
||||||
console.info("\tTXT\t" + ch.altname + "\t" + ch.dnsAuthorization);
|
);
|
||||||
console.info("");
|
console.info('');
|
||||||
|
console.info('\tTXT\t' + ch.altname + '\t' + ch.dnsAuthorization);
|
||||||
|
console.info('');
|
||||||
|
console.info('NOTE: the next get should be EMPTY');
|
||||||
|
console.info('');
|
||||||
|
|
||||||
return null;
|
setTimeout(function() {
|
||||||
|
cb(null, null);
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is implemented here for completeness (and perhaps some possible use in testing),
|
// This is implemented here for completeness (and perhaps some possible use in testing),
|
||||||
// but it's not something you would implement because the Greenlock server isn't the NameServer.
|
// but it's not something you would implement because the Greenlock server isn't the NameServer.
|
||||||
Challenge._getDns = function (args) {
|
Challenge._getDns = function(args) {
|
||||||
var ch = args.challenge;
|
var ch = args.challenge;
|
||||||
// because the way to mock a DNS challenge is weird
|
// because the way to mock a DNS challenge is weird
|
||||||
var altname = (ch.altname || ch.dnsHost || ch.identifier.value);
|
var altname = ch.altname || ch.dnsHost || ch.identifier.value;
|
||||||
var dnsHost = (ch.dnsHost || ch.identifier.value);
|
var dnsHost = ch.dnsHost || ch.identifier.value;
|
||||||
|
|
||||||
if (ch._test || !Challenge._getCache[ch.token]) {
|
if (ch._test || !Challenge._getCache[ch.token]) {
|
||||||
|
console.info('\n\n\n\n\n');
|
||||||
|
console.info('#################################');
|
||||||
|
console.info('# Step 3: Get Domain TXT Record #');
|
||||||
|
console.info('#################################');
|
||||||
Challenge._getCache[ch.token] = true;
|
Challenge._getCache[ch.token] = true;
|
||||||
console.info("");
|
console.info('');
|
||||||
console.info("[ACME " + ch.type + " '" + altname + "' REQUEST]: " + ch.status);
|
console.info(
|
||||||
|
'[ACME ' + ch.type + " '" + altname + "' REQUEST]: " + ch.status
|
||||||
|
);
|
||||||
console.info("The '" + ch.type + "' challenge request has arrived!");
|
console.info("The '" + ch.type + "' challenge request has arrived!");
|
||||||
console.info('dig TXT ' + dnsHost);
|
console.info('dig TXT ' + dnsHost);
|
||||||
console.info("(paste in the \"DNS Authorization\" you received a moment ago to respond)");
|
console.info(
|
||||||
process.stdout.write("> ");
|
'(paste in the "DNS Authorization" you received a moment ago to respond)'
|
||||||
|
);
|
||||||
|
process.stdout.write('> ');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
process.stdin.once('error', reject);
|
process.stdin.once('error', reject);
|
||||||
process.stdin.once('data', function (chunk) {
|
process.stdin.once('data', function(chunk) {
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
|
|
||||||
var result = chunk.toString('utf8').trim();
|
var result = chunk.toString('utf8').trim();
|
||||||
try {
|
try {
|
||||||
result = JSON.parse(result);
|
result = JSON.parse(result);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
args.challenge.dnsAuthorization = result;
|
args.challenge.dnsAuthorization = result;
|
||||||
result = args.challenge;
|
result = args.challenge;
|
||||||
}
|
}
|
||||||
if (result.dnsAuthorization) {
|
if (result.dnsAuthorization) {
|
||||||
resolve(result);
|
setTimeout(function() {
|
||||||
|
resolve(result);
|
||||||
|
}, 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The return value will checked. It must not be 'undefined'.
|
// The return value will checked. It must not be 'undefined'.
|
||||||
resolve(null);
|
setTimeout(function() {
|
||||||
|
resolve(null);
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -127,15 +209,15 @@ Challenge._getCache = {};
|
||||||
|
|
||||||
function dnsChallengeToJson(ch) {
|
function dnsChallengeToJson(ch) {
|
||||||
return {
|
return {
|
||||||
type: ch.type
|
type: ch.type,
|
||||||
, altname: ch.altname
|
altname: ch.altname,
|
||||||
, identifier: ch.identifier
|
identifier: ch.identifier,
|
||||||
, wildcard: ch.wildcard
|
wildcard: ch.wildcard,
|
||||||
, expires: ch.expires
|
expires: ch.expires,
|
||||||
, token: ch.token
|
token: ch.token,
|
||||||
, thumbprint: ch.thumbprint
|
thumbprint: ch.thumbprint,
|
||||||
, keyAuthorization: ch.keyAuthorization
|
keyAuthorization: ch.keyAuthorization,
|
||||||
, dnsHost: ch.dnsHost
|
dnsHost: ch.dnsHost,
|
||||||
, dnsAuthorization: ch.dnsAuthorization
|
dnsAuthorization: ch.dnsAuthorization
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "acme-dns-01-cli",
|
"name": "acme-dns-01-cli",
|
||||||
"version": "3.0.7",
|
"version": "3.1.0",
|
||||||
"description": "A manual (interactive CLI) dns-based strategy for Greenlock / Let's Encrypt / ACME DNS-01 challenges",
|
"description": "A manual (interactive CLI) dns-based strategy for Greenlock / Let's Encrypt / ACME DNS-01 challenges",
|
||||||
"homepage": "https://greenlock.domains/",
|
"homepage": "https://greenlock.domains/",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -29,5 +29,8 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.rootprojects.org/root/acme-dns-01-cli.js/issues"
|
"url": "https://git.rootprojects.org/root/acme-dns-01-cli.js/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"acme-dns-01-test": "^3.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
24
test.js
24
test.js
|
@ -1,18 +1,22 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var tester = require('greenlock-challenge-test');
|
var tester = require('acme-dns-01-test');
|
||||||
|
|
||||||
var type = 'dns-01';
|
var type = 'dns-01';
|
||||||
var challenger = require('greenlock-challenge-dns').create({});
|
var challenger = require('./index.js').create({});
|
||||||
|
|
||||||
// The dry-run tests can pass on, literally, 'example.com'
|
// The dry-run tests can pass on, literally, 'example.com'
|
||||||
// but the integration tests require that you have control over the domain
|
// but the integration tests require that you have control over the domain
|
||||||
var domain = '*.example.com';
|
var zone = 'example.com';
|
||||||
|
|
||||||
tester.test(type, domain, challenger).then(function () {
|
tester
|
||||||
console.info("PASS");
|
// will test example.com, foo.example.com, *.foo.example.com
|
||||||
}).catch(function (err) {
|
.testZone(type, zone, challenger)
|
||||||
console.error("FAIL");
|
.then(function() {
|
||||||
console.error(err);
|
console.info('PASS');
|
||||||
process.exit(20);
|
})
|
||||||
});
|
.catch(function(err) {
|
||||||
|
console.error('FAIL');
|
||||||
|
console.error(err);
|
||||||
|
process.exit(20);
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue