v1.8: transitional support for v2.0

This commit is contained in:
AJ ONeal 2019-06-14 01:32:54 -06:00
parent dfbee8aa79
commit e6497fe34b
8 changed files with 482 additions and 324 deletions

19
.gitignore vendored
View File

@ -1,32 +1,17 @@
.env
*.pem *.pem
letsencrypt.work
letsencrypt.logs
letsencrypt.config
# Logs # Logs
logs logs
*.log *.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory # Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules node_modules

208
README.md
View File

@ -2,28 +2,77 @@
| [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js)
| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
| [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
|
| A [Root](https://therootcompany.com) Project # [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project
# [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) A **Zero (External) Dependency**\* library for building
Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates.
A lightweight, **Low Dependency**\* framework for building The primary goal of this library is to make it easy to
Let's Encrypt v2 (ACME draft 12) clients, successor to `le-acme-core.js`. get Accounts and Certificates through Let's Encrypt.
Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8).
\* <small>although `node-forge` and `ursa` are included as `optionalDependencies` # Features
for backwards compatibility with older versions of node, there are no other
dependencies except those that I wrote for this (and related) projects.</small> - [x] Let's Encrypt&trade; v2 / ACME Draft 12
- [ ] (in-progress) Let's Encrypt&trade; v2.1 / ACME Draft 18
- [ ] (in-progress) StartTLS Everywhere&trade;
- [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js)
- [x] **http-01** for single or multiple domains per certificate
- [x] **dns-01** for wildcards, localhost, private networks, etc
- [x] VanillaJS
- [x] Zero External Dependencies
- [x] Safe, Efficient, Maintained
- [x] Works in Node v6+
- [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains))
\* <small>The only required dependencies were built by us, specifically for this and related libraries.
There are some, truly optional, backwards-compatibility dependencies for node v6.</small>
## Looking for Quick 'n' Easy&trade;? ## Looking for Quick 'n' Easy&trade;?
If you're looking to _build a webserver_, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). If you want something that's more "batteries included" give
If you're looking for an _ACME-enabled webserver_, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
a try.
- [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) - [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
- [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js)
## v1.7+: Transitional v2 Support
By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18).
Although the draft 18 changes themselves don't requiring breaking the API,
we've been keeping backwards compatibility for a long time and the API has become messy.
We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify**
the code with a fresh new release.
As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js.
We've been really good about backwards compatibility for
## Recommended Example
Due to the upcoming changes we've removed the old documentation.
Instead we recommend that you take a look at the
[Digital Ocean DNS-01 Example](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
- [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js)
That's not exactly the new API, but it's close.
## Let's Encrypt v02 Directory URLs
```
# Production URL
https://acme-v02.api.letsencrypt.org/directory
```
```
# Staging URL
https://acme-staging-v02.api.letsencrypt.org/directory
```
<!--
## How to build ACME clients ## How to build ACME clients
As this is intended to build ACME clients, there is not a simple 2-line example As this is intended to build ACME clients, there is not a simple 2-line example
@ -63,136 +112,75 @@ examples/https-server.js
examples/http-server.js examples/http-server.js
``` ```
## Let's Encrypt Directory URLs -->
``` ## API
# Production URL
https://acme-v02.api.letsencrypt.org/directory
```
``` Status: Small, but breaking changes coming in v2
# Staging URL
https://acme-staging-v02.api.letsencrypt.org/directory
```
## Two API versions, Two Implementations
This library (acme-v2.js) supports ACME [_draft 11_](https://tools.ietf.org/html/draft-ietf-acme-acme-11),
otherwise known as Let's Encrypt v2 (or v02).
- ACME draft 11
- Let's Encrypt v2
- Let's Encrypt v02
The predecessor (le-acme-core) supports Let's Encrypt v1 (or v01), which was a
[hodge-podge of various drafts](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md)
of the ACME spec early on.
- ACME early draft
- Let's Encrypt v1
- Let's Encrypt v01
This library maintains compatibility with le-acme-core so that it can be used as a **drop-in replacement**
and requires **no changes to existing code**,
but also provides an updated API more congruent with draft 11.
## le-acme-core-compatible API (recommended)
Status: Stable, Locked, Bugfix-only
See Full Documentation at <https://git.coolaj86.com/coolaj86/le-acme-core.js>
```js
var RSA = require('rsa-compat').RSA;
var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA });
//
// Use exactly the same as le-acme-core
//
```
## Promise API (dev)
Status: Almost stable, but **not semver locked**
This API is a simple evolution of le-acme-core, This API is a simple evolution of le-acme-core,
but tries to provide a better mapping to the new draft 11 APIs. but tries to provide a better mapping to the new draft 11 APIs.
```js ```js
// Create Instance (Dependency Injection)
var ACME = require('acme-v2').ACME.create({ var ACME = require('acme-v2').ACME.create({
RSA: require('rsa-compat').RSA
// other overrides
, request: require('request')
, promisify: require('util').promisify
// used for constructing user-agent
, os: require('os')
, process: require('process')
// used for overriding the default user-agent // used for overriding the default user-agent
, userAgent: 'My custom UA String' userAgent: 'My custom UA String',
, getUserAgentString: function (deps) { return 'My custom UA String'; } getUserAgentString: function(deps) {
return 'My custom UA String';
},
// don't try to validate challenges locally // don't try to validate challenges locally
, skipChallengeTest: false skipChallengeTest: false,
skipDryRun: false,
// ask if the certificate can be issued up to 10 times before failing // ask if the certificate can be issued up to 10 times before failing
, retryPoll: 8 retryPoll: 8,
// ask if the certificate has been validated up to 6 times before cancelling // ask if the certificate has been validated up to 6 times before cancelling
, retryPending: 4 retryPending: 4,
// Wait 1000ms between retries // Wait 1000ms between retries
, retryInterval: 1000 retryInterval: 1000,
// Wait 10,000ms after deauthorizing a challenge before retrying // Wait 10,000ms after deauthorizing a challenge before retrying
, deauthWait: 10 * 1000 deauthWait: 10 * 1000
}); });
// Discover Directory URLs // Discover Directory URLs
ACME.init(acmeDirectoryUrl) // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}> ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}>
// Accounts // Accounts
ACME.accounts.create(options) // returns Promise<regr> registration data ACME.accounts.create(options); // returns Promise<regr> registration data
{ email: '<email>' // valid email (server checks MX records) options = {
, accountKeypair: { // privateKeyPem or privateKeyJwt email: '<email>', // valid email (server checks MX records)
accountKeypair: {
// privateKeyPem or privateKeyJwt
privateKeyPem: '<ASCII PEM>' privateKeyPem: '<ASCII PEM>'
} },
, agreeToTerms: fn (tosUrl) {} // returns Promise with tosUrl agreeToTerms: function(tosUrl) {} // should Promise the same `tosUrl` back
} };
// Registration // Registration
ACME.certificates.create(options) // returns Promise<pems={ privkey (key), cert, chain (ca) }> ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }>
{ newAuthzUrl: '<url>' // specify acmeUrls.newAuthz options = {
, newCertUrl: '<url>' // specify acmeUrls.newCert domainKeypair: {
, domainKeypair: {
privateKeyPem: '<ASCII PEM>' privateKeyPem: '<ASCII PEM>'
} },
, accountKeypair: { accountKeypair: {
privateKeyPem: '<ASCII PEM>' privateKeyPem: '<ASCII PEM>'
} },
, domains: [ 'example.com' ] domains: ['example.com'],
, setChallenge: fn (hostname, key, val) // return Promise getZones: function(opts) {}, // should Promise an array of domain zone names
, removeChallenge: fn (hostname, key) // return Promise setChallenge: function(opts) {}, // should Promise the record id, or name
} removeChallenge: function(opts) {} // should Promise null
``` };
Helpers & Stuff
```javascript
// Constants
ACME.challengePrefixes['http-01']; // '/.well-known/acme-challenge'
ACME.challengePrefixes['dns-01']; // '_acme-challenge'
``` ```
# Changelog # Changelog
- v1.8
- more transitional prepwork for new v2 API
- support newer (simpler) dns-01 and http-01 libraries
- v1.5 - v1.5
- perform full test challenge first (even before nonce) - perform full test challenge first (even before nonce)
- v1.3 - v1.3

View File

@ -0,0 +1,69 @@
(function(exports) {
'use strict';
// node[0] ./test.js[1] jon.doe@gmail.com[2] example.com,*.example.com[3] xxxxxx[4]
var email = process.argv[2] || process.env.ACME_EMAIL;
var domains = (process.argv[3] || process.env.ACME_DOMAINS).split(/[,\s]+/);
var token = process.argv[4] || process.env.DIGITALOCEAN_API_KEY;
// git clone https://git.rootprojects.org/root/acme-dns-01-digitalocean.js node_modules/acme-dns-01-digitalocean
var dns01 = require('acme-dns-01-digitalocean').create({
//baseUrl: 'https://api.digitalocean.com/v2/domains',
token: token
});
// This will be replaced with Keypairs.js in the next version
var promisify = require('util').promisify;
var generateKeypair = promisify(require('rsa-compat').RSA.generateKeypair);
//var ACME = exports.ACME || require('acme').ACME;
var ACME = exports.ACME || require('../').ACME;
var acme = ACME.create({});
acme
.init({
//directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory'
})
.then(function() {
return generateKeypair(null).then(function(accountPair) {
return generateKeypair(null).then(function(serverPair) {
return acme.accounts
.create({
// valid email (server checks MX records)
email: email,
accountKeypair: accountPair,
agreeToTerms: function(tosUrl) {
// ask user (if user is the host)
return tosUrl;
}
})
.then(function(account) {
console.info('Created Account:');
console.info(account);
return acme.certificates
.create({
domains: domains,
challenges: { 'dns-01': dns01 },
domainKeypair: serverPair,
accountKeypair: accountPair,
// v2 will be directly compatible with the new ACME modules,
// whereas this version needs a shim
getZones: dns01.zones,
setChallenge: dns01.set,
removeChallenge: dns01.remove
})
.then(function(certs) {
console.info('Secured SSL Certificates');
console.info(certs);
});
});
});
});
})
.catch(function(e) {
console.error('Something went wrong:');
console.error(e);
process.exit(500);
});
})('undefined' === typeof module ? window : module.exports);

3
examples/example.env Normal file
View File

@ -0,0 +1,3 @@
ACME_EMAIL=jon.doe@gmail.com
ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com
DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

111
node.js
View File

@ -276,7 +276,10 @@ ACME._registerAccount = function(me, options) {
} }
if (1 === options.agreeToTerms.length) { if (1 === options.agreeToTerms.length) {
// newer promise API // newer promise API
return options.agreeToTerms(me._tos).then(agree, reject); return Promise.resolve(options.agreeToTerms(me._tos)).then(
agree,
reject
);
} else if (2 === options.agreeToTerms.length) { } else if (2 === options.agreeToTerms.length) {
// backwards compat cb API // backwards compat cb API
return options.agreeToTerms(me._tos, function(err, tosUrl) { return options.agreeToTerms(me._tos, function(err, tosUrl) {
@ -461,6 +464,58 @@ ACME._chooseChallenge = function(options, results) {
return challenge; return challenge;
}; };
ACME._getZones = function(me, options, dnsHosts) {
if ('function' !== typeof options.getZones) {
options.getZones = function() {
return Promise.resolve([]);
};
}
return new Promise(function(resolve, reject) {
try {
if (options.getZones.length <= 1) {
options
.getZones({ dnsHosts: dnsHosts })
.then(resolve)
.catch(reject);
} else if (2 === options.getZones.length) {
options.getZones({ dnsHosts: dnsHosts }, function(err, zonenames) {
if (err) {
reject(err);
} else {
resolve(zonenames);
}
});
} else {
throw new Error(
'options.getZones should accept opts and Promise an array of zone names'
);
}
} catch (e) {
reject(e);
}
});
};
function newZoneRegExp(zonename) {
// (^|\.)example\.com$
// which matches:
// foo.example.com
// example.com
// but not:
// fooexample.com
return new RegExp('(^|\\.)' + zonename.replace(/\./g, '\\.') + '$');
}
function pluckZone(zonenames, dnsHost) {
return zonenames
.filter(function(zonename) {
// the only character that needs to be escaped for regex
// and is allowed in a domain name is '.'
return newZoneRegExp(zonename).test(dnsHost);
})
.sort(function(a, b) {
// longest match first
return b.length - a.length;
})[0];
}
ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
// we don't poison the dns cache with our dummy request // we don't poison the dns cache with our dummy request
var dnsPrefix = ACME.challengePrefixes['dns-01']; var dnsPrefix = ACME.challengePrefixes['dns-01'];
@ -490,6 +545,7 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
auth[key] = challenge[key]; auth[key] = challenge[key];
}); });
var zone = pluckZone(options.zonenames || [], auth.identifier.value);
// batteries-included helpers // batteries-included helpers
auth.hostname = auth.identifier.value; auth.hostname = auth.identifier.value;
// because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
@ -511,7 +567,15 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
.update(auth.keyAuthorization) .update(auth.keyAuthorization)
.digest('base64') .digest('base64')
); );
if (zone) {
auth.dnsZone = zone;
auth.dnsPrefix = auth.dnsHost
.replace(newZoneRegExp(zone), '')
.replace(/\.$/, '');
}
// for backwards compat
auth.challenge = auth;
return auth; return auth;
}; };
@ -997,6 +1061,15 @@ ACME._getCertificate = function(me, options) {
} }
} }
var dnsHosts = options.domains.map(function(d) {
return (
require('crypto')
.randomBytes(2)
.toString('hex') + d
);
});
return ACME._getZones(me, options, dnsHosts).then(function(zonenames) {
options.zonenames = zonenames;
// Do a little dry-run / self-test // Do a little dry-run / self-test
return ACME._testChallenges(me, options).then(function() { return ACME._testChallenges(me, options).then(function() {
if (me.debug) { if (me.debug) {
@ -1119,7 +1192,12 @@ ACME._getCertificate = function(me, options) {
); );
} }
var auth = ACME._challengeToAuth(me, options, results, challenge); var auth = ACME._challengeToAuth(
me,
options,
results,
challenge
);
auths.push(auth); auths.push(auth);
return ACME._setChallenge(me, options, auth).then(setNext); return ACME._setChallenge(me, options, auth).then(setNext);
}); });
@ -1156,7 +1234,9 @@ ACME._getCertificate = function(me, options) {
._request({ method: 'GET', url: me._certificate, json: true }) ._request({ method: 'GET', url: me._certificate, json: true })
.then(function(resp) { .then(function(resp) {
if (me.debug) { if (me.debug) {
console.debug('acme-v2: csr submitted and cert received:'); console.debug(
'acme-v2: csr submitted and cert received:'
);
} }
// https://github.com/certbot/certbot/issues/5721 // https://github.com/certbot/certbot/issues/5721
var certsarr = ACME.splitPemChain( var certsarr = ACME.splitPemChain(
@ -1180,6 +1260,7 @@ ACME._getCertificate = function(me, options) {
}); });
}); });
}); });
});
}; };
ACME.create = function create(me) { ACME.create = function create(me) {
@ -1190,7 +1271,7 @@ ACME.create = function create(me) {
me.challengePrefixes = ACME.challengePrefixes; me.challengePrefixes = ACME.challengePrefixes;
me.RSA = me.RSA || require('rsa-compat').RSA; me.RSA = me.RSA || require('rsa-compat').RSA;
//me.Keypairs = me.Keypairs || require('keypairs'); //me.Keypairs = me.Keypairs || require('keypairs');
me.request = me.request || require('@coolaj86/urequest'); me.request = me.request || require('@root/request');
me._dig = function(query) { me._dig = function(query) {
// TODO use digd.js // TODO use digd.js
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
@ -1241,7 +1322,27 @@ ACME.create = function create(me) {
} }
me.init = function(_directoryUrl) { me.init = function(_directoryUrl) {
me.directoryUrl = me.directoryUrl || _directoryUrl; if (_directoryUrl) {
_directoryUrl = _directoryUrl.directoryUrl || _directoryUrl;
}
if ('string' === typeof _directoryUrl) {
me.directoryUrl = _directoryUrl;
}
if (!me.directoryUrl) {
me.directoryUrl =
'https://acme-staging-v02.api.letsencrypt.org/directory';
console.warn();
console.warn(
"No ACME `directoryUrl` was specified. Using Let's Encrypt's staging environment as the default, which will issue invalid certs."
);
console.warn('\t' + me.directoryUrl);
console.warn();
console.warn(
"To get valid certificates you will need to switch to a production URL. You might like Let's Encrypt v2:"
);
console.warn('\t' + me.directoryUrl.replace('-staging', ''));
console.warn();
}
return ACME._directory(me).then(function(resp) { return ACME._directory(me).then(function(resp) {
me._directoryUrls = resp.body; me._directoryUrls = resp.body;
me._tos = me._directoryUrls.meta.termsOfService; me._tos = me._directoryUrls.meta.termsOfService;

22
package-lock.json generated
View File

@ -1,13 +1,19 @@
{ {
"name": "acme-v2", "name": "acme-v2",
"version": "1.7.6", "version": "1.8.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@coolaj86/urequest": { "@root/request": {
"version": "1.3.7", "version": "1.3.11",
"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" "integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
},
"dotenv": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==",
"dev": true
}, },
"eckles": { "eckles": {
"version": "1.4.1", "version": "1.4.1",
@ -29,9 +35,9 @@
"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" "integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw=="
}, },
"rsa-compat": { "rsa-compat": {
"version": "2.0.6", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz",
"integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", "integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==",
"requires": { "requires": {
"keypairs": "^1.2.14" "keypairs": "^1.2.14"
} }

View File

@ -1,11 +1,11 @@
{ {
"name": "acme-v2", "name": "acme-v2",
"version": "1.7.7", "version": "1.8.0",
"description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", "description": "A lightweight library for getting Free SSL certifications through Let's Encrypt, using the ACME protocol.",
"homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", "homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js",
"main": "node.js", "main": "node.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "node ./test.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -23,10 +23,13 @@
"automated https", "automated https",
"letsencrypt" "letsencrypt"
], ],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@coolaj86/urequest": "^1.3.6", "@root/request": "^1.3.11",
"rsa-compat": "^2.0.6" "rsa-compat": "^2.0.8"
},
"devDependencies": {
"dotenv": "^8.0.0"
} }
} }

3
test.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';
require('dotenv').config();
require('./examples/dns-01-digitalocean.js');