@small-tech/auto-encrypt 4.3.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,26 @@
1
- ////////////////////////////////////////////////////////////////////////////////
2
- //
3
- // Throws
4
- //
5
- // Extensible basic error creation and throwing methods that use symbols
6
- // and predefined yet configurable lists of errors to make working with errors
7
- // safer (fewer magic strings) and DRYer (Don’t Repeat Yourself).
8
- //
9
- // Copyright © 2020 Aral Balkan, Small Technology Foundation.
10
- // License: AGPLv3 or later.
11
- //
12
- ////////////////////////////////////////////////////////////////////////////////
1
+ /**
2
+
3
+ Throws
4
+
5
+ Extensible basic error creation and throwing methods that use symbols
6
+ and predefined yet configurable lists of errors to make working with errors
7
+ safer (fewer magic strings) and DRYer (Don’t Repeat Yourself).
8
+
9
+ Copyright © 2020 Aral Balkan, Small Technology Foundation.
10
+ License: AGPLv3 or later.
11
+ */
13
12
 
14
13
  import fs from 'fs'
15
14
 
16
- class SymbolicError extends Error {
15
+ export class SymbolicError extends Error {
17
16
  symbol = null
18
17
  }
19
18
 
19
+ /** @typedef { (str: string, hint?: string) => string } Hinted */
20
+
20
21
  // Add the ability to supply a custom hint to global errors. Hints are displayed
21
22
  // at the end of the error message, in parentheses.
23
+ /** @type {Hinted} */
22
24
  const hinted = (str, hint) => hint ? `${str} (${hint}).` : `${str}.`
23
25
 
24
26
  export default class Throws {
@@ -26,39 +28,55 @@ export default class Throws {
26
28
  // if it exists on the object the mixin() method is called with a reference to.
27
29
  static ERRORS = {
28
30
  [Symbol.for('UndefinedOrNullError')]:
31
+ /** @type {Hinted} */
29
32
  (object, hint) => hinted(`${object} must not be undefined or null`, hint),
30
33
 
31
34
  [Symbol.for('UndefinedError')]:
35
+ /** @type {Hinted} */
32
36
  (object, hint) => hinted(`${object} must not be undefined`, hint),
33
37
 
34
38
  [Symbol.for('NullError')]:
39
+ /** @type {Hinted} */
35
40
  (object, hint) => hinted(`${object} must not be null`, hint),
36
41
 
37
42
  [Symbol.for('ArgumentError')]:
43
+ /** @type {Hinted} */
38
44
  (object, hint) => hinted(`incorrect value for argument ${object}`, hint),
39
45
 
40
46
  [Symbol.for('MustBeInstantiatedViaAsyncFactoryMethodError')]:
41
- (object, hint) => hinted(`Use the static getInstanceAsync() method to initialise ${object}`),
47
+ /** @type {Hinted} */
48
+ (object, _hint) => hinted(`Use the static getInstanceAsync() method to initialise ${object}`),
42
49
 
43
50
  [Symbol.for('SingletonConstructorIsPrivateError')]:
44
- (object, hint) =>
45
- hinted(`{object} is a singleton; its constructor is private. Do not instantiate using new ${object}()`),
51
+ /** @type {Hinted} */
52
+ (object, _hint) =>
53
+ hinted(`${object} is a singleton; its constructor is private. Do not instantiate using new ${object}()`),
46
54
 
47
55
  [Symbol.for('StaticClassCannotBeInstantiatedError')]:
48
- (object, hint) => hinted(`${object} is a static class. You cannot instantiate it using new ${object}()`),
56
+ /** @type {Hinted} */
57
+ (object, _hint) => hinted(`${object} is a static class. You cannot instantiate it using new ${object}()`),
49
58
 
50
59
  [Symbol.for('ReadOnlyAccessorError')]:
51
- (object, hint) => hinted(`{object} is a read-only property`)
60
+ /** @type {Hinted} */
61
+ (object, _hint) => hinted(`${object} is a read-only property`)
52
62
  }
53
63
 
54
64
  constructor (customErrors = {}) {
55
65
  this.errors = Object.assign(customErrors, Throws.ERRORS)
56
66
  }
57
67
 
68
+ /**
69
+ @param {Symbol} symbol
70
+ @param {any[]} args
71
+ */
58
72
  messageFor (symbol, ...args) {
59
73
  return this.errors[symbol](...args)
60
74
  }
61
75
 
76
+ /**
77
+ @param {Symbol} symbol
78
+ @param {any[]} args
79
+ */
62
80
  createError (symbol, ...args) {
63
81
  const errorMessage = this.messageFor(symbol, ...args)
64
82
  const error = new SymbolicError(errorMessage)
@@ -68,12 +86,21 @@ export default class Throws {
68
86
  return error
69
87
  }
70
88
 
89
+ /**
90
+ @param {Boolean} condition
91
+ @param {Symbol} errorSymbol
92
+ @param {any[]} args
93
+ */
71
94
  if (condition, errorSymbol, ...args) {
72
95
  if (condition) {
73
96
  this.error(errorSymbol, ...args)
74
97
  }
75
98
  }
76
99
 
100
+
101
+ /**
102
+ @param {string} [parameterName]
103
+ */
77
104
  ifMissing (parameterName) {
78
105
  // Attempt to automatically get the argument name even if parameterName was not
79
106
  // manually specified in the code.
@@ -81,10 +108,11 @@ export default class Throws {
81
108
  // Thanks to Christian Bundy (https://social.coop/@christianbundy/) for the
82
109
  // technique (https://gist.github.com/christianbundy/4c3c29b9f1a52384d7e8a51a956227c2)
83
110
  // TODO: Generalise and use in the other parameter errors if it holds up to testing.
84
- const copy = Error.prepareStackTrace;
111
+ const originalPrepareStackTrace = Error.prepareStackTrace;
85
112
  Error.prepareStackTrace = (_, stack) => stack;
86
- const stack = new Error().stack;
87
- Error.prepareStackTrace = copy;
113
+ /** @type {NodeJS.CallSite[]} */
114
+ const stack = /** @type {NodeJS.CallSite[]} */ (/** @type {unknown} */ (new Error().stack))
115
+ Error.prepareStackTrace = originalPrepareStackTrace;
88
116
 
89
117
  const fileName = stack[1].getFileName().replace('file://', '');
90
118
  const lineNumber = stack[1].getLineNumber() - 1;
@@ -99,18 +127,30 @@ export default class Throws {
99
127
  this.error(Symbol.for('UndefinedOrNullError'), parameterName || argumentName || '<unknown>')
100
128
  }
101
129
 
130
+ /**
131
+ @param {any} object
132
+ @param {string} objectName
133
+ */
102
134
  ifUndefinedOrNull (object, objectName) {
103
135
  if (object == undefined) {
104
136
  this.error(Symbol.for('UndefinedOrNullError'), objectName)
105
137
  }
106
138
  }
107
139
 
140
+ /**
141
+ @param {any} object
142
+ @param {string} objectName
143
+ */
108
144
  ifUndefined (object, objectName) {
109
145
  if (object === undefined) {
110
146
  this.error(Symbol.for('UndefinedError'), objectName)
111
147
  }
112
148
  }
113
149
 
150
+ /**
151
+ @param {Symbol} symbol
152
+ @param {any[]} args
153
+ */
114
154
  error (symbol, ...args) {
115
155
  throw this.createError(symbol, ...args)
116
156
  }
@@ -1,25 +1,25 @@
1
- ////////////////////////////////////////////////////////////////////////////////
2
- //
3
- // asyncForEach by Sebastien Chopin
4
- //
5
- // https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
6
- //
7
- // Example:
8
- //
9
- // const waitFor = (ms) => new Promise(r => setTimeout(r, ms))
10
- //
11
- // async function main () {
12
- // await asyncForEach([1, 2, 3], async (num) => {
13
- // await waitFor(50)
14
- // console.log(num)
15
- // })
16
- // console.log('Done')
17
- // }
18
- //
19
- // main();
20
- //
21
- ////////////////////////////////////////////////////////////////////////////////
1
+ /**
2
+ asyncForEach by Sebastien Chopin
22
3
 
4
+ https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
5
+
6
+ @example
7
+
8
+ const waitFor = (ms) => new Promise(r => setTimeout(r, ms))
9
+
10
+ async function main () {
11
+ await asyncForEach([1, 2, 3], async (num) => {
12
+ await waitFor(50)
13
+ console.log(num)
14
+ })
15
+ console.log('Done')
16
+ }
17
+
18
+ main();
19
+
20
+ @param { any[] } array
21
+ @param { (...args: any[] ) => Promise<void> } callback
22
+ */
23
23
  export default async function asyncForEach(array, callback) {
24
24
  for (let index = 0; index < array.length; index++) {
25
25
  await callback(array[index], index, array);
@@ -0,0 +1,27 @@
1
+ import { Temporal } from 'temporal-polyfill'
2
+
3
+ /**
4
+ Helper to get “from now” string (e.g., “in 3 days” or “2 months ago”)
5
+
6
+ @param {Temporal.Instant} instant
7
+ @returns {string}
8
+ */
9
+ export default function fromNow (instant) {
10
+ const now = Temporal.Now.instant()
11
+ const duration = instant.since(now)
12
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
13
+
14
+ const seconds = duration.total({ unit: 'seconds' })
15
+ const absSeconds = Math.abs(seconds)
16
+
17
+ if (absSeconds < 60 * 60 * 23.5) {
18
+ const hours = Math.round(seconds / (60 * 60))
19
+ return rtf.format(hours, 'hour')
20
+ } else if (absSeconds < 60 * 60 * 24 * 29.5) {
21
+ const days = Math.round(seconds / (60 * 60 * 24))
22
+ return rtf.format(days, 'day')
23
+ } else {
24
+ const months = Math.round(seconds / (60 * 60 * 24 * 30))
25
+ return rtf.format(months, 'month')
26
+ }
27
+ }
package/lib/util/log.js CHANGED
@@ -1,4 +1,8 @@
1
- // Conditionally log to console.
1
+ /**
2
+ Conditionally log to console.
3
+
4
+ @param {any[]} args
5
+ */
2
6
  export default function log (...args) {
3
7
  if (process.env.QUIET) {
4
8
  return
@@ -1,3 +1,8 @@
1
+ /**
2
+ Returns a promise that resolves in the specified number of milliseconds.
3
+
4
+ @param { number } ms
5
+ */
1
6
  export default function waitFor(ms) {
2
7
  return new Promise(resolve => setTimeout(resolve, ms))
3
8
  }
@@ -1,15 +1,14 @@
1
- // Require @panva’s fork of ASN1 that’s already included as part of the Jose library.
1
+ // Require @panva’s fork of ASN1.
2
2
  // https://github.com/panva/asn1.js/
3
3
  import asn1 from '@panva/asn1.js';
4
4
 
5
5
  /**
6
- * RFC5280 X509 and Extension Definitions
7
- * From: https://github.com/indutny/asn1.js
8
- *
9
- * (Including this directly as the Jose library we use in Auto Encrypt
10
- * already includes a fork of ASN1.js but with this RFC implementation
11
- * stripped. There’s no reason to include the whole library again.)
12
- */
6
+ RFC5280 X509 and Extension Definitions
7
+ From: https://github.com/indutny/asn1.js
8
+
9
+ (Extended with support for 'extensionRequest' OID,
10
+ CertificationRequest and CertificationRequestInfo.)
11
+ */
13
12
 
14
13
  // OIDs
15
14
  const x509OIDs = {
@@ -35,7 +34,8 @@ const x509OIDs = {
35
34
  '2 5 29 46': 'freshestCRL',
36
35
  '2 5 29 54': 'inhibitAnyPolicy',
37
36
  '1 3 6 1 5 5 7 1 1': 'authorityInformationAccess',
38
- '1 3 6 1 5 5 7 11': 'subjectInformationAccess'
37
+ '1 3 6 1 5 5 7 11': 'subjectInformationAccess',
38
+ '1 2 840 113549 1 9 14': 'extensionRequest'
39
39
  };
40
40
 
41
41
  // CertificateList ::= SEQUENCE {
@@ -717,8 +717,8 @@ export const SubjectInformationAccess = asn1.define('SubjectInformationAccess',
717
717
  });
718
718
 
719
719
  /**
720
- * CRL Extensions
721
- */
720
+ CRL Extensions
721
+ */
722
722
 
723
723
  // CRLNumber ::= INTEGER
724
724
  export const CRLNumber = asn1.define('CRLNumber', function() {
@@ -786,6 +786,11 @@ export const CertificateIssuer = asn1.define('CertificateIssuer', function() {
786
786
  this.use(GeneralNames);
787
787
  });
788
788
 
789
+ // Extensions ::= SEQUENCE OF Extension
790
+ export const Extensions = asn1.define('Extensions', function() {
791
+ this.seqof(Extension)
792
+ })
793
+
789
794
  // OID label to extension model mapping
790
795
  const x509Extensions = {
791
796
  subjectDirectoryAttributes: SubjectDirectoryAttributes,
@@ -810,5 +815,35 @@ const x509Extensions = {
810
815
  freshestCRL: FreshestCRL,
811
816
  inhibitAnyPolicy: InhibitAnyPolicy,
812
817
  authorityInformationAccess: AuthorityInfoAccessSyntax,
813
- subjectInformationAccess: SubjectInformationAccess
818
+ subjectInformationAccess: SubjectInformationAccess,
819
+ extensionRequest: Extensions
814
820
  };
821
+
822
+ // PKCS#10 CertificationRequest (CSR)
823
+ // CertificationRequest ::= SEQUENCE {
824
+ // certificationRequestInfo CertificationRequestInfo,
825
+ // signatureAlgorithm AlgorithmIdentifier,
826
+ // signature BIT STRING
827
+ // }
828
+ export const CertificationRequest = asn1.define('CertificationRequest', function() {
829
+ this.seq().obj(
830
+ this.key('certificationRequestInfo').use(CertificationRequestInfo),
831
+ this.key('signatureAlgorithm').use(AlgorithmIdentifier),
832
+ this.key('signature').bitstr()
833
+ )
834
+ })
835
+
836
+ // CertificationRequestInfo ::= SEQUENCE {
837
+ // version INTEGER,
838
+ // subject Name,
839
+ // subjectPKInfo SubjectPublicKeyInfo,
840
+ // attributes [0] IMPLICIT Attributes
841
+ // }
842
+ export const CertificationRequestInfo = asn1.define('CertificationRequestInfo', function() {
843
+ this.seq().obj(
844
+ this.key('version').int(),
845
+ this.key('subject').use(Name),
846
+ this.key('subjectPKInfo').use(SubjectPublicKeyInfo),
847
+ this.key('attributes').implicit(0).setof(Attribute)
848
+ )
849
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@small-tech/auto-encrypt",
3
- "version": "4.3.0",
3
+ "version": "5.0.0",
4
4
  "description": "Automatically provisions and renews Let’s Encrypt TLS certificates on Node.js https servers (including Kitten, Polka, Express.js, etc.)",
5
5
  "engines": {
6
6
  "node": ">=18.20.0"
@@ -24,7 +24,10 @@
24
24
  "license": "AGPL-3.0-or-later",
25
25
  "type": "module",
26
26
  "main": "index.js",
27
+ "types": "index.d.ts",
27
28
  "files": [
29
+ "index.js",
30
+ "index.d.ts",
28
31
  "lib",
29
32
  "typedefs"
30
33
  ],
@@ -51,7 +54,7 @@
51
54
  "coverage-pebble-sleep": "PEBBLE_WFE_NONCEREJECT=0 QUIET=true c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
52
55
  "coverage-pebble-sleep-noncereject": "QUIET=true c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
53
56
  "coverage-staging": "STAGING=true QUIET=true c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
54
- "generate-dependency-diagram": "node_modules/.bin/depcruise --exclude \"^node_modules|lib/util|typedefs|^https|^crypto$|^fs$|^os$|^tls$|^path$|^events$|^http$|^util\" --output-type dot index.js | dot -T svg > artefacts/dependency-graph.svg",
57
+ "generate-dependency-diagram": "node_modules/.bin/depcruise --exclude \"^node_modules|lib/util|typedefs|^https|^crypto$|^fs$|^os$|^tls$|^path$|^events$|^http$|^util\" --output-type dot index.js | dot -T svg > artifacts/dependency-graph.svg",
55
58
  "generate-developer-documentation": "npm run generate-dependency-diagram && node_modules/.bin/jsdoc2md --private --template developer-documentation.hbs --files typedefs/**/*.js --files index.js --files lib/*.js > developer-documentation.md"
56
59
  },
57
60
  "c8": {
@@ -64,20 +67,18 @@
64
67
  ]
65
68
  },
66
69
  "dependencies": {
67
- "bent": "^7.3.12",
70
+ "@panva/asn1.js": "^1.0.0",
68
71
  "encodeurl": "^1.0.2",
69
- "jose": "^1.28.2",
70
- "moment": "^2.29.4",
71
- "node-forge": "^1.3.1",
72
- "ocsp": "^1.2.0"
72
+ "temporal-polyfill": "^0.3.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@small-tech/esm-tape-runner": "^1.0.3",
76
- "@small-tech/node-pebble": "^5.1.3",
77
- "@small-tech/tap-monkey": "^1.3.0",
78
- "c8": "^7.12.0",
76
+ "@small-tech/node-pebble": "^5.5.2",
77
+ "@small-tech/tap-monkey": "^1.6.1",
78
+ "@types/node": "^25.0.9",
79
+ "c8": "^10.1.3",
79
80
  "dependency-cruiser": "^12.3.0",
80
- "jsdoc-to-markdown": "^6.0.1",
81
+ "jsdoc-to-markdown": "^9.1.3",
81
82
  "tape": "^5.2.1"
82
83
  }
83
84
  }
@@ -1,37 +1,48 @@
1
1
  // These are types that are not explicitly defined in JavaScript but used in the AcmeRequest class.
2
2
 
3
3
  /**
4
- * @typedef {Object} PreparedRequest
5
- *
6
- * @property {ProtectedHeader} protectedHeader JSON Web Signature (JWS) Protected Header
7
- * (See RFC 7515 § A.6.1)
8
- * @property {JWS.FlattenedJWS} signedRequest Flattened JWS
9
- * @property {bent.RequestFunction<bent.ValidResponse>} httpsRequest Asynchronous HTTPs request,
10
- * ready to be executed.
11
- * @property {HttpsHeaders} httpsHeaders Hardcoded HTTPS headers.
4
+ @typedef {Object} PreparedRequest
5
+ @property {ProtectedHeader} protectedHeader - JSON Web Signature (JWS) Protected Header (See RFC 7515 § A.6.1)
6
+ @property {FlattenedJWS} signedRequest - Flattened JWS.
7
+ @property {Function} httpsRequest - Asynchronous HTTPs request, ready to be executed.
8
+ @property {HttpsHeaders} httpsHeaders - Hardcoded HTTPS headers.
9
+ @property {any[]} originalRequestDetails - The original details used to prepare the request.
12
10
  */
13
11
 
14
12
  /**
15
- * @typedef ProtectedHeader
16
- * @property {String} alg Hardcoded to 'RS256', currently the only algorithm supported by Let’s Encrypt (LE).
17
- * @property {String} nonce {@link Nonce} (a value that’s used only once to thwart replay attacks).
18
- * @property {String} url URL of the command on Let’s Encrypt’s servers.
19
- * @property {String} kid Key ID returned by LE (per RFC 8555 § 6.2, set either this or jwk, not both).
20
- * @property {JWKRSAKey} jwk Public JWK (per RFC 8555 § 6.2, set either this or jwk, not both).
21
- */
13
+ @typedef {Object} FlattenedJWS
14
+ @property {string} protected - Base64url encoded protected header.
15
+ @property {string} payload - Base64url encoded payload.
16
+ @property {string} signature - Base64url encoded signature.
17
+ */
22
18
 
23
19
  /**
24
- * @typedef {Object} HttpsHeaders
25
- *
26
- * @property {String} 'Content-Type' Hardcoded to 'application/jose+json'
27
- * @property {String} 'User-Agent' Hardcoded to 'small-tech.org-acme/1.0.0 node/12.16.0'
28
- * @property {String} 'Accept-Language Hardcoded to 'en-US'
29
- */
20
+ @typedef ProtectedHeader
21
+ @property {string} alg - Hardcoded to 'RS256', currently the only algorithm supported by Let’s Encrypt (LE).
22
+ @property {string} nonce - {@link Nonce} (a value that’s used only once to thwart replay attacks).
23
+ @property {string} url - URL of the command on Let’s Encrypt’s servers.
24
+ @property {string} [kid] - Key ID returned by LE (per RFC 8555 § 6.2, set either this or jwk, not both).
25
+ @property {JWK} [jwk] - Public JWK (per RFC 8555 § 6.2, set either this or jwk, not both).
26
+ */
27
+
28
+ /**
29
+ @typedef {Object} JWK
30
+ @property {string} kty
31
+ @property {string} n
32
+ @property {string} e
33
+ */
30
34
 
31
35
  /**
32
- * @typedef ResponseObject
33
- * @property {Object} headers Native HTTPS response headers object.
34
- * @property {Object|String} body The response body as a native object or as a string.
35
- */
36
+ @typedef {Object} HttpsHeaders
37
+ @property {string} `Content-Type` - Hardcoded to 'application/jose+json'
38
+ @property {string} `User-Agent` - Hardcoded to 'small-tech.org-acme/1.0.0 node/12.16.0'
39
+ @property {string} `Accept-Language` - Hardcoded to 'en-US'
40
+ */
41
+
42
+ /**
43
+ @typedef ResponseObject
44
+ @property {Object} headers - Native HTTPS response headers object.
45
+ @property {Object|String} body - The response body as a native object or as a string.
46
+ */
36
47
 
37
48
  export default {}