@tryghost/domain-validator 3.0.0 → 4.0.1

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/README.md CHANGED
@@ -1,19 +1,23 @@
1
1
  # Domain Validator
2
2
 
3
3
  ## Installation
4
+
4
5
  1. Make sure that `gstenv` is green. See [Dev Environment](https://github.com/TryGhost/Team/blob/master/Engineering/Dev%20Environment.md) for docs.
5
6
  2. `git clone` this repo & `cd` into it as usual
6
7
  3. Run `yarn` to install top-level dependencies.
7
8
 
8
9
  ## Run
10
+
9
11
  - Use: `yarn dev`
10
12
  - View: [http://localhost:9999](http://localhost:9999)
11
13
 
12
14
  ## Test
15
+
13
16
  - `yarn lint` run just eslint
14
17
  - `yarn test` run lint && tests
15
18
 
16
19
  ## Publish
20
+
17
21
  - `yarn ship`
18
22
 
19
23
  # Copyright & License
package/bin/dv.js CHANGED
@@ -3,7 +3,7 @@ const validator = require('../');
3
3
 
4
4
  prettyCLI
5
5
  .configure({
6
- name: 'dv'
6
+ name: 'dv',
7
7
  })
8
8
  .groupOrder([
9
9
  'Sources:',
@@ -12,20 +12,19 @@ prettyCLI
12
12
  'Arguments:',
13
13
  'Required Options:',
14
14
  'Options:',
15
- 'Global Options:'
15
+ 'Global Options:',
16
16
  ])
17
17
  .positional('<domain>', {
18
18
  paramsDesc: 'The domain you want to check',
19
- mustExist: true
19
+ mustExist: true,
20
20
  })
21
21
  .positional('<subdomain>', {
22
22
  paramsDesc: 'The expected Ghost(Pro) subdomain',
23
- mustExist: true
23
+ mustExist: true,
24
24
  })
25
25
  .parseAndExit()
26
26
  .then((argv) => {
27
27
  validator
28
28
  .validate(argv.subdomain, argv.domain)
29
- .then(report => prettyCLI.ui.log(require('util').inspect(report, false, null))
30
- );
29
+ .then((report) => prettyCLI.ui.log(require('util').inspect(report, false, null)));
31
30
  });
package/bin/req.js CHANGED
@@ -3,7 +3,7 @@ const request = require('../lib/request');
3
3
 
4
4
  prettyCLI
5
5
  .configure({
6
- name: 'req'
6
+ name: 'req',
7
7
  })
8
8
  .groupOrder([
9
9
  'Sources:',
@@ -12,16 +12,18 @@ prettyCLI
12
12
  'Arguments:',
13
13
  'Required Options:',
14
14
  'Options:',
15
- 'Global Options:'
15
+ 'Global Options:',
16
16
  ])
17
17
  .positional('<domain>', {
18
18
  paramsDesc: 'The domain you want to request',
19
- mustExist: true
19
+ mustExist: true,
20
20
  })
21
21
  .parseAndExit()
22
22
  .then((argv) => {
23
23
  request
24
24
  .domain(argv.domain)
25
- .then(res => prettyCLI.ui.log('result', require('util').inspect(res, false, null)))
26
- .catch(err => prettyCLI.ui.log.error('Error', require('util').inspect(err, false, null)));
25
+ .then((res) => prettyCLI.ui.log('result', require('util').inspect(res, false, null)))
26
+ .catch((err) =>
27
+ prettyCLI.ui.log.error('Error', require('util').inspect(err, false, null)),
28
+ );
27
29
  });
package/lib/caa.js CHANGED
@@ -2,36 +2,42 @@ const dns = require('@tryghost/dns');
2
2
  const debug = require('@tryghost/debug')('dv:caa');
3
3
 
4
4
  /**
5
- * Check if the domain's CAA records allow the given issuer.
5
+ * Check whether the domain's CAA records allow any of the given issuers.
6
6
  *
7
7
  * @param {string} customdomain
8
- * @param {string} issuer - CA identity to check (e.g. 'certainly.com')
9
- * @returns {Promise<{isCaaValid: boolean, caaSource: string|null, caaAllowedIssuers: string[], caaError: string|null}>}
8
+ * @param {string[]} issuers - CA identities in preference order (e.g. ['certainly.com', 'letsencrypt.org'])
9
+ * @returns {Promise<{isCaaValid: boolean, caaChosenIssuer: string|null, caaSource: string|null, caaAllowedIssuers: string[], caaError: string|null}>}
10
10
  */
11
- module.exports.validate = (customdomain, issuer) => {
12
- debug('Checking CAA for ' + customdomain + ' against ' + issuer);
13
- return dns.caa(customdomain, {issuer}).then((result) => {
14
- return {
15
- isCaaValid: result.allowed,
16
- caaSource: result.source,
17
- caaAllowedIssuers: result.allowedIssuers,
18
- caaError: result.error
19
- };
20
- }).catch((err) => {
21
- // Fail open: dns.caa fails open internally on DNS errors, but guard
22
- // against unexpected throws (bad arguments, future regressions) so
23
- // CAA can never take down the rest of the validity report. Tolerate
24
- // non-Error rejections (null/undefined/strings) so this handler
25
- // itself can never throw.
26
- const message = (err && typeof err.message === 'string')
27
- ? err.message
28
- : String(err || 'UNKNOWN');
29
- debug('Unexpected CAA error: ' + message);
30
- return {
31
- isCaaValid: true,
32
- caaSource: null,
33
- caaAllowedIssuers: [],
34
- caaError: message
35
- };
36
- });
11
+ module.exports.validate = (customdomain, issuers) => {
12
+ // Tolerate a missing issuers arg here so we still enter the .catch below
13
+ // and fail open. dns.caa will throw a clear ValidationError on bad input.
14
+ debug('Checking CAA for ' + customdomain + ' against ' + (issuers || []).join(', '));
15
+ return dns
16
+ .caa(customdomain, { issuers })
17
+ .then((result) => {
18
+ return {
19
+ isCaaValid: result.allowed,
20
+ caaChosenIssuer: result.matchedIssuer,
21
+ caaSource: result.source,
22
+ caaAllowedIssuers: result.allowedIssuers,
23
+ caaError: result.error,
24
+ };
25
+ })
26
+ .catch((err) => {
27
+ // Fail open: dns.caa fails open internally on DNS errors, but guard
28
+ // against unexpected throws (bad arguments, future regressions) so
29
+ // CAA can never take down the rest of the validity report. Tolerate
30
+ // non-Error rejections (null/undefined/strings) so this handler
31
+ // itself can never throw.
32
+ const message =
33
+ err && typeof err.message === 'string' ? err.message : String(err || 'UNKNOWN');
34
+ debug('Unexpected CAA error: ' + message);
35
+ return {
36
+ isCaaValid: true,
37
+ caaChosenIssuer: issuers && issuers[0] ? issuers[0] : null,
38
+ caaSource: null,
39
+ caaAllowedIssuers: [],
40
+ caaError: message,
41
+ };
42
+ });
37
43
  };
package/lib/dns.js CHANGED
@@ -10,7 +10,7 @@ const responseObject = {
10
10
  isDnsValidForSSL: false,
11
11
  sslRating: null,
12
12
  message: null,
13
- raw: null
13
+ raw: null,
14
14
  };
15
15
 
16
16
  // Ghost(Pro) specific logic, using extra values
@@ -20,28 +20,32 @@ _private.handleDnsReport = function handleDnsReport(dnsResult, subdomain, isGhos
20
20
  const response = _.clone(responseObject);
21
21
 
22
22
  switch (dnsResult.validity.dns) {
23
- // CASE: correct - we have a Ghost(Pro) subdomain, check it against what we expect
24
- case 'correct':
25
- // CASE: mismatch - there is a CNAME, but it's not what we expected
26
- if (cname !== subdomain) {
27
- // CNAME is incorrect, revise the validity
28
- dnsResult.validity.dns = 'mismatch';
29
- dnsResult.validity.ssl = 'D';
30
- dnsResult.validity.reason = `CNAME mismatch, ${prodomain} subdomain is incorrect.`
31
- + ' Expected: ' + subdomain + ' Actual: ' + cname;
32
- }
33
- break;
34
- // CASE: obfuscated - we need to do one last check before giving up on this!
35
- case 'obfuscated':
36
- if (isGhostPro) {
37
- dnsResult.validity.ssl = 'B';
38
- dnsResult.validity.reason = 'DNS is obfuscated, but does resolve to Ghost(Pro)';
39
- } else {
40
- dnsResult.validity.ssl = 'D';
41
- dnsResult.validity.dns = 'missing';
42
- dnsResult.validity.reason = 'DNS is obfuscated and does not resolve to Ghost(Pro)';
43
- }
44
- break;
23
+ // CASE: correct - we have a Ghost(Pro) subdomain, check it against what we expect
24
+ case 'correct':
25
+ // CASE: mismatch - there is a CNAME, but it's not what we expected
26
+ if (cname !== subdomain) {
27
+ // CNAME is incorrect, revise the validity
28
+ dnsResult.validity.dns = 'mismatch';
29
+ dnsResult.validity.ssl = 'D';
30
+ dnsResult.validity.reason =
31
+ `CNAME mismatch, ${prodomain} subdomain is incorrect.` +
32
+ ' Expected: ' +
33
+ subdomain +
34
+ ' Actual: ' +
35
+ cname;
36
+ }
37
+ break;
38
+ // CASE: obfuscated - we need to do one last check before giving up on this!
39
+ case 'obfuscated':
40
+ if (isGhostPro) {
41
+ dnsResult.validity.ssl = 'B';
42
+ dnsResult.validity.reason = 'DNS is obfuscated, but does resolve to Ghost(Pro)';
43
+ } else {
44
+ dnsResult.validity.ssl = 'D';
45
+ dnsResult.validity.dns = 'missing';
46
+ dnsResult.validity.reason = 'DNS is obfuscated and does not resolve to Ghost(Pro)';
47
+ }
48
+ break;
45
49
  }
46
50
 
47
51
  response.isDnsValid = dnsResult.validity.dns === 'correct';
@@ -56,9 +60,8 @@ _private.handleDnsReport = function handleDnsReport(dnsResult, subdomain, isGhos
56
60
 
57
61
  module.exports.validate = (subdomain, customdomain, isGhostPro, prodomain) => {
58
62
  debug('Validating ' + customdomain);
59
- return dns(customdomain, {cname: prodomain})
60
- .then((dnsResult) => {
61
- debug('Response received');
62
- return _private.handleDnsReport(dnsResult, subdomain, isGhostPro, prodomain);
63
- });
63
+ return dns(customdomain, { cname: prodomain }).then((dnsResult) => {
64
+ debug('Response received');
65
+ return _private.handleDnsReport(dnsResult, subdomain, isGhostPro, prodomain);
66
+ });
64
67
  };
package/lib/pro.js CHANGED
@@ -13,18 +13,21 @@ const responseObject = {
13
13
  isProxy: false,
14
14
  isProxyValid: null,
15
15
  // [proxyErrors], optional
16
- raw: null
16
+ raw: null,
17
17
  };
18
18
 
19
19
  // Attempt to detect if the results look like a proxy
20
20
  _private.detectProxy = (headers, proto) => {
21
21
  if (
22
22
  // If x-forwarded-host was originally set, this is a proxy. "- [Invalid]" means original value was empty
23
- (_.has(headers, 'x-ghost-forwarded-host') && headers['x-ghost-forwarded-host'] !== '- [Invalid]')
23
+ (_.has(headers, 'x-ghost-forwarded-host') &&
24
+ headers['x-ghost-forwarded-host'] !== '- [Invalid]') ||
24
25
  // If x-amz-cf-id is set, then this is cloudfront and therefore is a proxy
25
- || _.has(headers, 'x-amz-cf-id')
26
+ _.has(headers, 'x-amz-cf-id') ||
26
27
  // If we are making an https request and get http back, this must be a proxy
27
- || (proto === 'https' && _.has(headers, 'x-ghost-forwarded-proto') && headers['x-ghost-forwarded-proto'] !== proto)
28
+ (proto === 'https' &&
29
+ _.has(headers, 'x-ghost-forwarded-proto') &&
30
+ headers['x-ghost-forwarded-proto'] !== proto)
28
31
  ) {
29
32
  return true;
30
33
  }
@@ -35,43 +38,49 @@ _private.detectProxy = (headers, proto) => {
35
38
  _private.validateProxy = (headers, subdomain, customdomain, subdirectory, prodomain) => {
36
39
  const proxyResult = {
37
40
  isValid: true,
38
- errors: []
41
+ errors: [],
39
42
  };
40
43
 
41
44
  // Forwarded Host must match the custom domain
42
- if (!_.has(headers, 'x-ghost-forwarded-host') || headers['x-ghost-forwarded-host'] !== customdomain) {
45
+ if (
46
+ !_.has(headers, 'x-ghost-forwarded-host') ||
47
+ headers['x-ghost-forwarded-host'] !== customdomain
48
+ ) {
43
49
  proxyResult.isValid = false;
44
- proxyResult.errors.push(
45
- {
46
- 'x-forwarded-host': {
47
- actual: headers['x-ghost-forwarded-host'],
48
- expected: customdomain
49
- }
50
- });
50
+ proxyResult.errors.push({
51
+ 'x-forwarded-host': {
52
+ actual: headers['x-ghost-forwarded-host'],
53
+ expected: customdomain,
54
+ },
55
+ });
51
56
  }
52
57
 
53
58
  // Proto must be https
54
- if (!_.has(headers, 'x-ghost-forwarded-proto') || headers['x-ghost-forwarded-proto'] !== 'https') {
59
+ if (
60
+ !_.has(headers, 'x-ghost-forwarded-proto') ||
61
+ headers['x-ghost-forwarded-proto'] !== 'https'
62
+ ) {
55
63
  proxyResult.isValid = false;
56
- proxyResult.errors.push(
57
- {
58
- 'x-forwarded-proto': {
59
- actual: headers['x-ghost-forwarded-proto'],
60
- expected: 'https'
61
- }
62
- });
64
+ proxyResult.errors.push({
65
+ 'x-forwarded-proto': {
66
+ actual: headers['x-ghost-forwarded-proto'],
67
+ expected: 'https',
68
+ },
69
+ });
63
70
  }
64
71
 
65
72
  // Host must match the pro domain
66
- if (!_.has(headers, 'x-ghost-host') || headers['x-ghost-host'] !== `${subdomain}.${prodomain}`) {
73
+ if (
74
+ !_.has(headers, 'x-ghost-host') ||
75
+ headers['x-ghost-host'] !== `${subdomain}.${prodomain}`
76
+ ) {
67
77
  proxyResult.isValid = false;
68
- proxyResult.errors.push(
69
- {
70
- host: {
71
- actual: headers['x-ghost-host'],
72
- expected: `${subdomain}.${prodomain}`
73
- }
74
- });
78
+ proxyResult.errors.push({
79
+ host: {
80
+ actual: headers['x-ghost-host'],
81
+ expected: `${subdomain}.${prodomain}`,
82
+ },
83
+ });
75
84
  }
76
85
 
77
86
  // If we have a subdirectory, this _must_ be a proxy
@@ -79,8 +88,8 @@ _private.validateProxy = (headers, subdomain, customdomain, subdirectory, prodom
79
88
  proxyResult.errors.push({
80
89
  subdirectory: {
81
90
  actual: 'invalid proxy',
82
- expected: 'valid proxy'
83
- }
91
+ expected: 'valid proxy',
92
+ },
84
93
  });
85
94
  }
86
95
 
@@ -111,7 +120,9 @@ _private.buildResponse = (result, proto, subdomain, customdomain, subdirectory,
111
120
  if (subdomain && prodomain) {
112
121
  validRedirectDomains.push(`${subdomain}.${prodomain}`);
113
122
  }
114
- response.isGhostProRedirect = response.redirect.every(redirect => validRedirectDomains.includes(new URL(redirect.headers.location).hostname));
123
+ response.isGhostProRedirect = response.redirect.every((redirect) =>
124
+ validRedirectDomains.includes(new URL(redirect.headers.location).hostname),
125
+ );
115
126
  }
116
127
 
117
128
  // If we find the x-ghost-cache-status header, we're seeing Ghost(Pro)
@@ -123,12 +134,19 @@ _private.buildResponse = (result, proto, subdomain, customdomain, subdirectory,
123
134
  }
124
135
 
125
136
  // Ensure that we only set isGhostPro to true if the redirect only uses valid domains
126
- response.isGhostPro = response.isGhostPro && (!response.hasRedirect || response.isGhostProRedirect);
137
+ response.isGhostPro =
138
+ response.isGhostPro && (!response.hasRedirect || response.isGhostProRedirect);
127
139
 
128
140
  response.isProxy = _private.detectProxy(headers, proto);
129
141
 
130
142
  // Generate a proxy report
131
- const proxyResult = _private.validateProxy(headers, subdomain, customdomain, subdirectory, prodomain);
143
+ const proxyResult = _private.validateProxy(
144
+ headers,
145
+ subdomain,
146
+ customdomain,
147
+ subdirectory,
148
+ prodomain,
149
+ );
132
150
 
133
151
  // If we have a detectable proxy, or a subdirectory, we must have a valid proxy config
134
152
  if (response.isProxy || subdirectory) {
@@ -146,29 +164,43 @@ _private.buildResponse = (result, proto, subdomain, customdomain, subdirectory,
146
164
  };
147
165
 
148
166
  // Predicate check to see if we should test HTTP
149
- _private.isCertificateError = error => /certificate/.test(error.context);
167
+ _private.isCertificateError = (error) => /certificate/.test(error.context);
150
168
 
151
169
  // Checks a Pro site using the provided details, and returning a responseObject
152
170
  module.exports.validate = (proto, subdomain, customdomain, subdirectory, prodomain) => {
153
171
  const firstAttemptUrl = _private.buildUrl(proto, customdomain, subdirectory);
154
172
 
155
173
  debug('Validating ' + firstAttemptUrl);
156
- return request.domain(firstAttemptUrl)
174
+ return request
175
+ .domain(firstAttemptUrl)
157
176
  .then((result) => {
158
177
  debug('Response received');
159
- return _private.buildResponse(result, proto, subdomain, customdomain, subdirectory, prodomain);
178
+ return _private.buildResponse(
179
+ result,
180
+ proto,
181
+ subdomain,
182
+ customdomain,
183
+ subdirectory,
184
+ prodomain,
185
+ );
160
186
  })
161
187
  .catch(() => {
162
188
  const secondAttemptUrl = _private.buildUrl('http', customdomain, subdirectory);
163
189
 
164
190
  debug('Validating ' + secondAttemptUrl);
165
- return request.domain(secondAttemptUrl)
166
- .then((result) => {
167
- debug('HTTP response received');
168
- const response = _private.buildResponse(result, 'http', subdomain, customdomain, subdirectory, prodomain);
169
- response.isHTTPOnly = true;
170
- return response;
171
- });
191
+ return request.domain(secondAttemptUrl).then((result) => {
192
+ debug('HTTP response received');
193
+ const response = _private.buildResponse(
194
+ result,
195
+ 'http',
196
+ subdomain,
197
+ customdomain,
198
+ subdirectory,
199
+ prodomain,
200
+ );
201
+ response.isHTTPOnly = true;
202
+ return response;
203
+ });
172
204
  })
173
205
  .catch((error) => {
174
206
  const response = _.clone(responseObject);
package/lib/request.js CHANGED
@@ -7,7 +7,7 @@ const debug = require('@tryghost/debug')('dv:request');
7
7
  // Use 3rd party DNS resolver
8
8
  const http = require('http');
9
9
  const https = require('https');
10
- const {Resolver} = require('dns').promises;
10
+ const { Resolver } = require('dns').promises;
11
11
  const resolver = new Resolver();
12
12
  resolver.setServers(['1.1.1.1', '1.0.0.1']);
13
13
  const semverMajor = require('semver/functions/major');
@@ -18,12 +18,14 @@ const staticLookup = () => async (hostname, _options, callback) => {
18
18
  const ips = await resolver.resolve(hostname);
19
19
 
20
20
  if (ips.length === 0) {
21
- return callback(new errors.InternalServerError({message: `Unable to resolve ${hostname}`}));
21
+ return callback(
22
+ new errors.InternalServerError({ message: `Unable to resolve ${hostname}` }),
23
+ );
22
24
  }
23
25
 
24
26
  if (nodeVersion >= 20) {
25
27
  // Node 20+ uses a different callback signature
26
- callback(null, [{address: ips[0], family: 4}]);
28
+ callback(null, [{ address: ips[0], family: 4 }]);
27
29
  } else {
28
30
  // Node 18 and below use the old callback signature
29
31
  callback(null, ips[0], 4);
@@ -42,19 +44,19 @@ module.exports.domain = (url) => {
42
44
  followRedirect: false,
43
45
  // Don't allow the request to go on for more than 15 seconds
44
46
  timeout: {
45
- request: 15000
47
+ request: 15000,
46
48
  },
47
49
  // If the network errors, retry 1 time:
48
50
  retry: {
49
- limit: 1
51
+ limit: 1,
50
52
  },
51
53
  headers: {
52
- 'X-Ghost-Proxy': true
54
+ 'X-Ghost-Proxy': true,
53
55
  },
54
56
  agent: {
55
- http: new http.Agent({lookup: staticLookup()}),
56
- https: new https.Agent({lookup: staticLookup()})
57
- }
57
+ http: new http.Agent({ lookup: staticLookup() }),
58
+ https: new https.Agent({ lookup: staticLookup() }),
59
+ },
58
60
  };
59
61
 
60
62
  const responses = [];
@@ -66,14 +68,21 @@ module.exports.domain = (url) => {
66
68
  const statusCode = res.statusCode;
67
69
  const headers = {};
68
70
 
69
- _.reduce(res.headers, (updatedHeaders = {}, value, name) => {
70
- if (_.startsWith(name, 'x-ghost') || _.includes(['server', 'location', 'x-amz-cf-id'], name)) {
71
- updatedHeaders[name] = value;
72
- }
73
- return updatedHeaders;
74
- }, headers);
71
+ _.reduce(
72
+ res.headers,
73
+ (updatedHeaders = {}, value, name) => {
74
+ if (
75
+ _.startsWith(name, 'x-ghost') ||
76
+ _.includes(['server', 'location', 'x-amz-cf-id'], name)
77
+ ) {
78
+ updatedHeaders[name] = value;
79
+ }
80
+ return updatedHeaders;
81
+ },
82
+ headers,
83
+ );
75
84
 
76
- responses.push({statusCode, headers});
85
+ responses.push({ statusCode, headers });
77
86
 
78
87
  if (statusCode.toString()[0] === '3') {
79
88
  numHops += 1;
@@ -82,23 +91,25 @@ module.exports.domain = (url) => {
82
91
  return;
83
92
  }
84
93
 
85
- return got.head(res.headers.location, options)
86
- .then(handleResponse);
94
+ return got.head(res.headers.location, options).then(handleResponse);
87
95
  }
88
96
  }
89
97
 
90
98
  return new Promise(function (resolve, reject) {
91
99
  debug('starting request');
92
- return got.head(url, options)
100
+ return got
101
+ .head(url, options)
93
102
  .then(handleResponse)
94
103
  .then(() => resolve(responses))
95
104
  .catch((error) => {
96
- reject(new errors.BadRequestError({
97
- message: 'Domain could not be requested',
98
- property: url,
99
- context: error.message,
100
- code: error.code
101
- }));
105
+ reject(
106
+ new errors.BadRequestError({
107
+ message: 'Domain could not be requested',
108
+ property: url,
109
+ context: error.message,
110
+ code: error.code,
111
+ }),
112
+ );
102
113
  });
103
114
  });
104
115
  };
package/lib/validate.js CHANGED
@@ -11,7 +11,7 @@ const validityReportObject = {
11
11
  input: {
12
12
  subdomain: '',
13
13
  customdomain: '',
14
- subdirectory: ''
14
+ subdirectory: '',
15
15
  },
16
16
  isResponding: false,
17
17
  isHTTPOnly: false,
@@ -24,22 +24,26 @@ const validityReportObject = {
24
24
  isDnsValidForSSL: false,
25
25
  sslRating: null,
26
26
  isCaaValid: true,
27
+ caaChosenIssuer: null,
27
28
  caaSource: null,
28
29
  caaAllowedIssuers: [],
29
30
  caaError: null,
30
- message: ''
31
+ message: '',
31
32
  };
32
33
 
33
- _private.doChecks = (subdomain, customdomain, subdirectory, prodomain, caaIssuer) => {
34
+ _private.doChecks = (subdomain, customdomain, subdirectory, prodomain, caaIssuers) => {
34
35
  const checkResult = {};
35
36
  debug('starting pro');
36
- return pro.validate('https', subdomain, customdomain, subdirectory, prodomain)
37
+ return pro
38
+ .validate('https', subdomain, customdomain, subdirectory, prodomain)
37
39
  .then((proResult) => {
38
40
  checkResult.pro = proResult;
39
41
  debug('got pro, starting dns');
40
42
  return Promise.all([
41
43
  dns.validate(subdomain, customdomain, checkResult.pro.isGhostPro, prodomain),
42
- caaIssuer ? caa.validate(customdomain, caaIssuer) : Promise.resolve(null)
44
+ caaIssuers && caaIssuers.length > 0
45
+ ? caa.validate(customdomain, caaIssuers)
46
+ : Promise.resolve(null),
43
47
  ]);
44
48
  })
45
49
  .then(([dnsResult, caaResult]) => {
@@ -60,51 +64,66 @@ _private.doChecks = (subdomain, customdomain, subdirectory, prodomain, caaIssuer
60
64
  * @param {string} customdomain
61
65
  * @param {string} subdirectory
62
66
  * @param {string} [prodomain=ghost.io]
63
- * @param {string} [caaIssuer] - CA identity to check CAA records against (e.g. 'certainly.com').
64
- * When omitted, CAA is not checked and isCaaValid defaults to true.
67
+ * @param {string[]} [caaIssuers] - CA identities to check CAA records against, in preference order
68
+ * (e.g. ['certainly.com', 'letsencrypt.org']). When omitted or
69
+ * empty, CAA is not checked and isCaaValid defaults to true.
65
70
  */
66
- module.exports.validate = (subdomain, customdomain, subdirectory, prodomain = 'ghost.io', caaIssuer) => _private
67
- .doChecks(subdomain, customdomain, subdirectory, prodomain, caaIssuer)
68
- .then(function (result) {
69
- // Deep clone so nested mutable fields (input, caaAllowedIssuers)
70
- // can't leak across validation calls via the shared template.
71
- const validityReport = _.cloneDeep(validityReportObject);
71
+ module.exports.validate = (
72
+ subdomain,
73
+ customdomain,
74
+ subdirectory,
75
+ prodomain = 'ghost.io',
76
+ caaIssuers,
77
+ ) =>
78
+ _private
79
+ .doChecks(subdomain, customdomain, subdirectory, prodomain, caaIssuers)
80
+ .then(function (result) {
81
+ // Deep clone so nested mutable fields (input, caaAllowedIssuers)
82
+ // can't leak across validation calls via the shared template.
83
+ const validityReport = _.cloneDeep(validityReportObject);
72
84
 
73
- // Store the inputted details
74
- validityReport.input.subdomain = subdomain;
75
- validityReport.input.customdomain = customdomain;
76
- validityReport.input.subdirectory = subdirectory;
85
+ // Store the inputted details
86
+ validityReport.input.subdomain = subdomain;
87
+ validityReport.input.customdomain = customdomain;
88
+ validityReport.input.subdirectory = subdirectory;
77
89
 
78
- // Copy over all the values from result.pro
79
- _.merge(validityReport, _.omit(result.pro, 'raw'));
80
- // Do a few last DNS-related checks, and then output validity values
81
- _.merge(validityReport, _.omit(result.dns, 'raw'));
90
+ // Copy over all the values from result.pro
91
+ _.merge(validityReport, _.omit(result.pro, 'raw'));
92
+ // Do a few last DNS-related checks, and then output validity values
93
+ _.merge(validityReport, _.omit(result.dns, 'raw'));
82
94
 
83
- if (result.caa) {
84
- _.merge(validityReport, result.caa);
95
+ if (result.caa) {
96
+ _.merge(validityReport, result.caa);
85
97
 
86
- // CAA blocking issuance means SSL can't be provisioned going forward,
87
- // even if DNS currently points at Ghost — treat it like a DNS failure.
88
- if (!validityReport.isCaaValid) {
89
- validityReport.isDnsValidForSSL = false;
90
- validityReport.sslRating = 'D';
91
- validityReport.message = 'CAA record does not allow ' + caaIssuer
92
- + ' (allowed: ' + (validityReport.caaAllowedIssuers.join(', ') || 'none')
93
- + ', source: ' + validityReport.caaSource + ')';
98
+ // CAA blocking issuance means SSL can't be provisioned going forward,
99
+ // even if DNS currently points at Ghost — treat it like a DNS failure.
100
+ if (!validityReport.isCaaValid) {
101
+ validityReport.isDnsValidForSSL = false;
102
+ validityReport.sslRating = 'D';
103
+ validityReport.message =
104
+ 'CAA record does not allow any of ' +
105
+ caaIssuers.join(', ') +
106
+ ' (allowed: ' +
107
+ (validityReport.caaAllowedIssuers.join(', ') || 'none') +
108
+ ', source: ' +
109
+ validityReport.caaSource +
110
+ ')';
111
+ }
94
112
  }
95
- }
96
113
 
97
- if (validityReport.isProxy) {
98
- const proxyMessage = validityReport.isProxyValid ? 'Found a valid proxy' : 'Found invalid proxy config';
99
- validityReport.message = validityReport.message
100
- ? validityReport.message + '; ' + proxyMessage
101
- : proxyMessage;
102
- }
114
+ if (validityReport.isProxy) {
115
+ const proxyMessage = validityReport.isProxyValid
116
+ ? 'Found a valid proxy'
117
+ : 'Found invalid proxy config';
118
+ validityReport.message = validityReport.message
119
+ ? validityReport.message + '; ' + proxyMessage
120
+ : proxyMessage;
121
+ }
103
122
 
104
- validityReport.raw = {
105
- pro: result.pro.raw,
106
- dns: result.dns.raw
107
- };
123
+ validityReport.raw = {
124
+ pro: result.pro.raw,
125
+ dns: result.dns.raw,
126
+ };
108
127
 
109
- return validityReport;
110
- });
128
+ return validityReport;
129
+ });
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@tryghost/domain-validator",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
+ "license": "MIT",
5
+ "author": "Ghost Foundation",
4
6
  "repository": {
5
7
  "type": "git",
6
- "url": "ssh://git@github.com/TryGhost/Pro",
8
+ "url": "ssh://git@github.com/TryGhost/Pro-Packages",
7
9
  "directory": "packages/domain-validator"
8
10
  },
9
- "author": "Ghost Foundation",
10
- "license": "MIT",
11
- "main": "lib/validate.js",
12
11
  "bin": {
13
12
  "dv": "./bin/dv.js"
14
13
  },
@@ -17,26 +16,27 @@
17
16
  "lib",
18
17
  "index.js"
19
18
  ],
20
- "scripts": {
21
- "dv": "node bin/dv.js",
22
- "req": "node bin/req.js",
23
- "lint": "eslint . --ext .js --cache",
24
- "posttest": "yarn lint",
25
- "test": "NODE_ENV=testing mocha -- $(find test -name '*.test.js')"
26
- },
27
- "devDependencies": {
28
- "mocha": "11.7.5",
29
- "should": "13.2.3",
30
- "sinon": "21.0.3"
31
- },
19
+ "main": "lib/validate.js",
32
20
  "dependencies": {
33
21
  "@tryghost/debug": "2.0.3",
34
- "@tryghost/dns": "^0.7.0",
35
22
  "@tryghost/errors": "3.0.3",
36
23
  "@tryghost/pretty-cli": "3.0.3",
37
24
  "bluebird": "3.7.2",
38
25
  "got": "11.8.6",
39
- "lodash": "4.17.23"
26
+ "lodash": "4.17.23",
27
+ "semver": "7.7.4",
28
+ "@tryghost/dns": "^0.8.1"
29
+ },
30
+ "devDependencies": {
31
+ "mocha": "11.7.5",
32
+ "should": "13.2.3",
33
+ "sinon": "21.0.3"
40
34
  },
41
- "gitHead": "c5d1ebb864969de89c1e8a8dcac3f577bceee1dd"
42
- }
35
+ "scripts": {
36
+ "dv": "node bin/dv.js",
37
+ "req": "node bin/req.js",
38
+ "lint": "oxlint -c ../../.oxlintrc.json .",
39
+ "posttest": "pnpm lint",
40
+ "test": "NODE_ENV=testing mocha -- $(find test -name '*.test.js')"
41
+ }
42
+ }