@jsenv/https-local 1.1.0 → 3.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.
package/README.md CHANGED
@@ -61,19 +61,18 @@ node ./install_certificate_authority.mjs
61
61
  *
62
62
  * > node ./install_certificate_authority.mjs
63
63
  *
64
- * Read more in https://github.com/jsenv/https-local#requestCertificateForLocalhost
64
+ * Read more in https://github.com/jsenv/https-local#requestCertificate
65
65
  */
66
66
 
67
67
  import { createServer } from "node:https"
68
- import { requestCertificateForLocalhost } from "@jsenv/https-local"
68
+ import { requestCertificate } from "@jsenv/https-local"
69
69
 
70
- const { serverCertificate, serverCertificatePrivateKey } =
71
- await requestCertificateForLocalhost()
70
+ const { certificate, privateKey } = requestCertificate()
72
71
 
73
72
  const server = createServer(
74
73
  {
75
- cert: serverCertificate,
76
- key: serverCertificatePrivateKey,
74
+ cert: certificate,
75
+ key: privateKey,
77
76
  },
78
77
  (request, response) => {
79
78
  const body = "Hello world"
@@ -100,10 +99,10 @@ The rest of the documentation goes into details.
100
99
 
101
100
  # Certificate expiration
102
101
 
103
- | Certificate | Expires after | How to renew? |
104
- | ----------- | ------------- | --------------------------------------- |
105
- | server | 1 year | Re-run _requestCertificateForLocalhost_ |
106
- | authority | 20 year | Re-run _installCertificateAuthority_ |
102
+ | Certificate | Expires after | How to renew? |
103
+ | ----------- | ------------- | ------------------------------------ |
104
+ | server | 1 year | Re-run _requestCertificate_ |
105
+ | authority | 20 year | Re-run _installCertificateAuthority_ |
107
106
 
108
107
  The **server** certificate expires after one year which is the maximum duration allowed by web browsers. In the unlikely scenario where your local server is running for more than a year without interruption, restart it and you're good for one more year.
109
108
 
@@ -353,18 +352,17 @@ Check if certificate is trusted by firefox...
353
352
 
354
353
  </details>
355
354
 
356
- # requestCertificateForLocalhost
355
+ # requestCertificate
357
356
 
358
- _requestCertificateForLocalhost_ function returns a certificate and private key that can be used to start a server in HTTPS.
357
+ _requestCertificate_ function returns a certificate and private key that can be used to start a server in HTTPS.
359
358
 
360
359
  ```js
361
360
  import { createServer } from "node:https"
362
- import { requestCertificateForLocalhost } from "@jsenv/https-local"
361
+ import { requestCertificate } from "@jsenv/https-local"
363
362
 
364
- const { serverCertificate, serverCertificatePrivateKey } =
365
- await requestCertificateForLocalhost({
366
- serverCertificateAltNames: ["localhost", "local.example"],
367
- })
363
+ const { certificate, privateKey } = requestCertificate({
364
+ altNames: ["localhost", "local.example"],
365
+ })
368
366
  ```
369
367
 
370
368
  [installCertificateAuthority](#installCertificateAuthority) must be called before this function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/https-local",
3
- "version": "1.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "A programmatic way to generate locally trusted certificates",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -21,14 +21,13 @@
21
21
  "type": "module",
22
22
  "exports": {
23
23
  ".": {
24
- "import": "./main.js"
24
+ "import": "./src/main.js"
25
25
  },
26
26
  "./*": "./*"
27
27
  },
28
- "main": "./main.js",
28
+ "main": "./src/main.js",
29
29
  "files": [
30
- "/src/",
31
- "/main.js"
30
+ "/src/"
32
31
  ],
33
32
  "scripts": {
34
33
  "eslint": "npx eslint . --ext=.js,.mjs,.cjs",
@@ -48,28 +47,25 @@
48
47
  "playwright-install": "npx playwright install-deps && npx playwright install"
49
48
  },
50
49
  "dependencies": {
51
- "@jsenv/filesystem": "4.0.2",
52
- "@jsenv/log": "1.6.3",
53
- "@jsenv/logger": "4.1.1",
54
- "@jsenv/urls": "1.1.2",
50
+ "@jsenv/filesystem": "4.1.2",
51
+ "@jsenv/log": "3.1.0",
52
+ "@jsenv/urls": "1.2.7",
55
53
  "command-exists": "1.2.9",
56
54
  "node-forge": "1.3.1",
57
55
  "sudo-prompt": "9.2.1",
58
56
  "which": "2.0.2"
59
57
  },
60
58
  "devDependencies": {
61
- "@jsenv/assert": "2.5.4",
62
- "@jsenv/core": "27.0.0-alpha.69",
63
- "@jsenv/eslint-config": "16.0.9",
64
- "@jsenv/eslint-import-resolver": "0.1.11",
65
- "@jsenv/github-release-package": "1.4.0",
66
- "@jsenv/importmap-eslint-resolver": "5.2.5",
67
- "@jsenv/importmap-node-module": "5.1.3",
68
- "@jsenv/package-publish": "1.7.5",
69
- "@jsenv/performance-impact": "2.2.11",
70
- "eslint": "8.17.0",
59
+ "@jsenv/assert": "2.6.0",
60
+ "@jsenv/core": "28.0.2",
61
+ "@jsenv/eslint-config": "16.2.1",
62
+ "@jsenv/eslint-import-resolver": "0.3.0",
63
+ "@jsenv/github-release-package": "1.5.0",
64
+ "@jsenv/package-publish": "1.10.0",
65
+ "@jsenv/performance-impact": "2.3.0",
66
+ "eslint": "8.21.0",
71
67
  "eslint-plugin-import": "2.26.0",
72
- "playwright": "1.22.2",
73
- "prettier": "2.7.0"
68
+ "playwright": "1.24.2",
69
+ "prettier": "2.7.1"
74
70
  }
75
71
  }
@@ -1,14 +1,13 @@
1
1
  import { readFile, writeFile, removeEntry } from "@jsenv/filesystem"
2
- import { createLogger, createDetailedMessage } from "@jsenv/logger"
3
- import { UNICODE } from "@jsenv/log"
2
+ import { UNICODE, createLogger, createDetailedMessage } from "@jsenv/log"
4
3
 
4
+ import { forge } from "./internal/forge.js"
5
5
  import { getAuthorityFileInfos } from "./internal/authority_file_infos.js"
6
6
  import { attributeDescriptionFromAttributeArray } from "./internal/certificate_data_converter.js"
7
7
  import {
8
8
  formatTimeDelta,
9
9
  formatDuration,
10
10
  } from "./internal/validity_formatting.js"
11
- import { importNodeForge } from "./internal/forge.js"
12
11
  import { createAuthorityRootCertificate } from "./internal/certificate_generator.js"
13
12
  import { importPlatformMethods } from "./internal/platform.js"
14
13
  import { jsenvParameters } from "./jsenvParameters.js"
@@ -79,7 +78,7 @@ export const installCertificateAuthority = async ({
79
78
  serialNumber: 0,
80
79
  })
81
80
 
82
- const { pki } = await importNodeForge()
81
+ const { pki } = forge
83
82
  const rootCertificate = pemAsFileContent(
84
83
  pki.certificateToPem(rootCertificateForgeObject),
85
84
  )
@@ -173,7 +172,7 @@ export const installCertificateAuthority = async ({
173
172
  const rootCertificate = await readFile(rootCertificateFileInfo.path, {
174
173
  as: "string",
175
174
  })
176
- const { pki } = await importNodeForge()
175
+ const { pki } = forge
177
176
  const rootCertificateForgeObject = pki.certificateFromPem(rootCertificate)
178
177
 
179
178
  logger.info(`Checking certificate validity...`)
@@ -232,13 +231,12 @@ export const installCertificateAuthority = async ({
232
231
  const rootCertificatePrivateKeyForgeObject = pki.privateKeyFromPem(
233
232
  rootCertificatePrivateKey,
234
233
  )
235
-
236
234
  const trustInfo = await platformMethods.executeTrustQuery({
237
235
  logger,
238
236
  certificateCommonName,
239
237
  certificateFileUrl: rootCertificateFileInfo.url,
240
238
  certificate: rootCertificate,
241
- verb: tryToTrust ? "ADD_TRUST" : "CHECK_TRUST",
239
+ verb: tryToTrust ? "ENSURE_TRUST" : "CHECK_TRUST",
242
240
  NSSDynamicInstall,
243
241
  })
244
242
 
@@ -323,7 +321,7 @@ export const uninstallCertificateAuthority = async ({
323
321
  const rootCertificate = await readFile(rootCertificateFileInfo.url, {
324
322
  as: "string",
325
323
  })
326
- const { pki } = await importNodeForge()
324
+ const { pki } = forge
327
325
  const rootCertificateForgeObject = pki.certificateFromPem(rootCertificate)
328
326
  const rootCertificateCommonName = attributeDescriptionFromAttributeArray(
329
327
  rootCertificateForgeObject.subject.attributes,
@@ -1,39 +1,38 @@
1
- import { readFile, writeFile } from "@jsenv/filesystem"
2
- import { createLogger, createDetailedMessage } from "@jsenv/logger"
3
- import { UNICODE } from "@jsenv/log"
1
+ import { readFileSync } from "node:fs"
2
+ import { writeFileSync } from "@jsenv/filesystem"
3
+ import { UNICODE, createLogger, createDetailedMessage } from "@jsenv/log"
4
4
 
5
+ import { forge } from "./internal/forge.js"
5
6
  import {
6
7
  createValidityDurationOfXDays,
7
8
  verifyServerCertificateValidityDuration,
8
9
  } from "./validity_duration.js"
9
10
  import { getAuthorityFileInfos } from "./internal/authority_file_infos.js"
10
- import { importNodeForge } from "./internal/forge.js"
11
11
  import { requestCertificateFromAuthority } from "./internal/certificate_generator.js"
12
12
  import { formatDuration } from "./internal/validity_formatting.js"
13
13
 
14
- export const requestCertificateForLocalhost = async ({
14
+ export const requestCertificate = ({
15
15
  logLevel,
16
16
  logger = createLogger({ logLevel }), // to be able to catch logs during unit tests
17
17
 
18
- serverCertificateAltNames = ["localhost"],
19
- serverCertificateCommonName = "https local server certificate",
20
- serverCertificateValidityDurationInMs = createValidityDurationOfXDays(396),
18
+ altNames = ["localhost"],
19
+ commonName = "https local server certificate",
20
+ validityDurationInMs = createValidityDurationOfXDays(396),
21
21
  } = {}) => {
22
- if (typeof serverCertificateValidityDurationInMs !== "number") {
22
+ if (typeof validityDurationInMs !== "number") {
23
23
  throw new TypeError(
24
- `serverCertificateValidityDurationInMs must be a number but received ${serverCertificateValidityDurationInMs}`,
24
+ `validityDurationInMs must be a number but received ${validityDurationInMs}`,
25
25
  )
26
26
  }
27
- if (serverCertificateValidityDurationInMs < 1) {
27
+ if (validityDurationInMs < 1) {
28
28
  throw new TypeError(
29
- `serverCertificateValidityDurationInMs must be > 0 but received ${serverCertificateValidityDurationInMs}`,
29
+ `validityDurationInMs must be > 0 but received ${validityDurationInMs}`,
30
30
  )
31
31
  }
32
- const validityDurationInfo = verifyServerCertificateValidityDuration(
33
- serverCertificateValidityDurationInMs,
34
- )
32
+ const validityDurationInfo =
33
+ verifyServerCertificateValidityDuration(validityDurationInMs)
35
34
  if (!validityDurationInfo.ok) {
36
- serverCertificateValidityDurationInMs = validityDurationInfo.maxAllowedValue
35
+ validityDurationInMs = validityDurationInfo.maxAllowedValue
37
36
  logger.warn(
38
37
  createDetailedMessage(validityDurationInfo.message, {
39
38
  details: validityDurationInfo.details,
@@ -48,7 +47,7 @@ export const requestCertificateForLocalhost = async ({
48
47
  } = getAuthorityFileInfos()
49
48
  if (!rootCertificateFileInfo.exists) {
50
49
  throw new Error(
51
- `Certificate authority not found, "installCertificateAuthority" must be called before "requestCertificateForLocalhost"`,
50
+ `Certificate authority not found, "installCertificateAuthority" must be called before "requestServerCertificate"`,
52
51
  )
53
52
  }
54
53
  if (!rootCertificatePrivateKeyFileInfo.exists) {
@@ -59,19 +58,16 @@ export const requestCertificateForLocalhost = async ({
59
58
  }
60
59
 
61
60
  logger.debug(`Restoring certificate authority from filesystem...`)
62
- const { pki } = await importNodeForge()
63
- const rootCertificate = await readFile(rootCertificateFileInfo.url, {
64
- as: "string",
65
- })
66
- const rootCertificatePrivateKey = await readFile(
67
- rootCertificatePrivateKeyFileInfo.url,
68
- {
69
- as: "string",
70
- },
61
+ const { pki } = forge
62
+ const rootCertificate = String(
63
+ readFileSync(new URL(rootCertificateFileInfo.url)),
64
+ )
65
+ const rootCertificatePrivateKey = String(
66
+ readFileSync(new URL(rootCertificatePrivateKeyFileInfo.url)),
67
+ )
68
+ const certificateAuthorityData = JSON.parse(
69
+ String(readFileSync(new URL(authorityJsonFileInfo.url))),
71
70
  )
72
- const certificateAuthorityData = await readFile(authorityJsonFileInfo.url, {
73
- as: "json",
74
- })
75
71
  const rootCertificateForgeObject = pki.certificateFromPem(rootCertificate)
76
72
  const rootCertificatePrivateKeyForgeObject = pki.privateKeyFromPem(
77
73
  rootCertificatePrivateKey,
@@ -80,26 +76,22 @@ export const requestCertificateForLocalhost = async ({
80
76
 
81
77
  const serverCertificateSerialNumber =
82
78
  certificateAuthorityData.serialNumber + 1
83
- await writeFile(
79
+ writeFileSync(
84
80
  authorityJsonFileInfo.url,
85
81
  JSON.stringify({ serialNumber: serverCertificateSerialNumber }, null, " "),
86
82
  )
87
83
 
88
- if (!serverCertificateAltNames.includes("localhost")) {
89
- serverCertificateAltNames.push("localhost")
90
- }
91
-
92
84
  logger.debug(`Generating server certificate...`)
93
85
  const { certificateForgeObject, certificatePrivateKeyForgeObject } =
94
- await requestCertificateFromAuthority({
86
+ requestCertificateFromAuthority({
95
87
  logger,
96
88
  authorityCertificateForgeObject: rootCertificateForgeObject,
97
89
  auhtorityCertificatePrivateKeyForgeObject:
98
90
  rootCertificatePrivateKeyForgeObject,
99
91
  serialNumber: serverCertificateSerialNumber,
100
- altNames: serverCertificateAltNames,
101
- commonName: serverCertificateCommonName,
102
- validityDurationInMs: serverCertificateValidityDurationInMs,
92
+ altNames,
93
+ commonName,
94
+ validityDurationInMs,
103
95
  })
104
96
  const serverCertificate = pki.certificateToPem(certificateForgeObject)
105
97
  const serverCertificatePrivateKey = pki.privateKeyToPem(
@@ -109,13 +101,13 @@ export const requestCertificateForLocalhost = async ({
109
101
  `${
110
102
  UNICODE.OK
111
103
  } server certificate generated, it will be valid for ${formatDuration(
112
- serverCertificateValidityDurationInMs,
104
+ validityDurationInMs,
113
105
  )}`,
114
106
  )
115
107
 
116
108
  return {
117
- serverCertificate,
118
- serverCertificatePrivateKey,
109
+ certificate: serverCertificate,
110
+ privateKey: serverCertificatePrivateKey,
119
111
  rootCertificateFilePath: rootCertificateFileInfo.path,
120
112
  }
121
113
  }
@@ -1,5 +1,4 @@
1
- import { createDetailedMessage, createLogger } from "@jsenv/logger"
2
- import { UNICODE } from "@jsenv/log"
1
+ import { createDetailedMessage, createLogger, UNICODE } from "@jsenv/log"
3
2
 
4
3
  import {
5
4
  HOSTS_FILE_PATH,
@@ -1,7 +1,7 @@
1
1
  // https://github.com/digitalbazaar/forge/blob/master/examples/create-cert.js
2
2
  // https://github.com/digitalbazaar/forge/issues/660#issuecomment-467145103
3
3
 
4
- import { importNodeForge } from "./forge.js"
4
+ import { forge } from "./forge.js"
5
5
  import {
6
6
  attributeArrayFromAttributeDescription,
7
7
  attributeDescriptionFromAttributeArray,
@@ -23,7 +23,6 @@ export const createAuthorityRootCertificate = async ({
23
23
  throw new TypeError(`serial must be a number but received ${serialNumber}`)
24
24
  }
25
25
 
26
- const forge = await importNodeForge()
27
26
  const { pki } = forge
28
27
  const rootCertificateForgeObject = pki.createCertificate()
29
28
  const keyPair = pki.rsa.generateKeyPair(2048) // TODO: use async version https://github.com/digitalbazaar/forge#rsa
@@ -86,7 +85,7 @@ export const createAuthorityRootCertificate = async ({
86
85
  }
87
86
  }
88
87
 
89
- export const requestCertificateFromAuthority = async ({
88
+ export const requestCertificateFromAuthority = ({
90
89
  authorityCertificateForgeObject, // could be intermediate or root certificate authority
91
90
  auhtorityCertificatePrivateKeyForgeObject,
92
91
  serialNumber,
@@ -116,7 +115,6 @@ export const requestCertificateFromAuthority = async ({
116
115
  )
117
116
  }
118
117
 
119
- const forge = await importNodeForge()
120
118
  const { pki } = forge
121
119
  const certificateForgeObject = pki.createCertificate()
122
120
  const keyPair = pki.rsa.generateKeyPair(2048) // TODO: use async version https://github.com/digitalbazaar/forge#rsa
@@ -2,6 +2,4 @@ import { createRequire } from "node:module"
2
2
 
3
3
  const require = createRequire(import.meta.url)
4
4
 
5
- export const importNodeForge = async () => {
6
- return require("node-forge")
7
- }
5
+ export const forge = require("node-forge")
@@ -1,8 +1,9 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { execSync } from "node:child_process"
3
3
  import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
4
-
5
4
  import { UNICODE } from "@jsenv/log"
5
+
6
+ import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"
6
7
  import {
7
8
  nssCommandName,
8
9
  detectIfNSSIsInstalled,
@@ -10,8 +11,6 @@ import {
10
11
  getCertutilBinPath,
11
12
  } from "./nss_linux.js"
12
13
 
13
- import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"
14
-
15
14
  export const executeTrustQueryOnChrome = ({
16
15
  logger,
17
16
  certificateCommonName,
@@ -3,6 +3,7 @@ import { execSync } from "node:child_process"
3
3
  import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
4
4
  import { UNICODE } from "@jsenv/log"
5
5
 
6
+ import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"
6
7
  import {
7
8
  nssCommandName,
8
9
  detectIfNSSIsInstalled,
@@ -10,8 +11,6 @@ import {
10
11
  getCertutilBinPath,
11
12
  } from "./nss_linux.js"
12
13
 
13
- import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"
14
-
15
14
  export const executeTrustQueryOnFirefox = ({
16
15
  logger,
17
16
  certificateCommonName,
@@ -4,16 +4,16 @@
4
4
 
5
5
  import { existsSync } from "node:fs"
6
6
  import { fileURLToPath } from "node:url"
7
- import { createDetailedMessage } from "@jsenv/logger"
8
7
  import { readFile } from "@jsenv/filesystem"
9
- import { UNICODE } from "@jsenv/log"
8
+ import { createDetailedMessage, UNICODE } from "@jsenv/log"
10
9
 
11
- import { exec } from "@jsenv/https-local/src/internal/exec.js"
12
10
  import {
13
11
  VERB_CHECK_TRUST,
14
12
  VERB_ADD_TRUST,
13
+ VERB_ENSURE_TRUST,
15
14
  VERB_REMOVE_TRUST,
16
15
  } from "../trust_query.js"
16
+ import { exec } from "../exec.js"
17
17
 
18
18
  const REASON_NEW_AND_TRY_TO_TRUST_DISABLED =
19
19
  "certificate is new and tryToTrust is disabled"
@@ -101,7 +101,11 @@ export const executeTrustQueryOnLinux = async ({
101
101
  }
102
102
 
103
103
  logger.info(`${UNICODE.OK} certificate found in linux`)
104
- if (verb === VERB_CHECK_TRUST || verb === VERB_ADD_TRUST) {
104
+ if (
105
+ verb === VERB_CHECK_TRUST ||
106
+ verb === VERB_ADD_TRUST ||
107
+ verb === VERB_ENSURE_TRUST
108
+ ) {
105
109
  return {
106
110
  status: "trusted",
107
111
  reason: REASON_FOUND_IN_LINUX,
@@ -1,7 +1,7 @@
1
1
  import { UNICODE } from "@jsenv/log"
2
2
 
3
- import { memoize } from "@jsenv/https-local/src/internal/memoize.js"
4
- import { exec } from "@jsenv/https-local/src/internal/exec.js"
3
+ import { memoize } from "../memoize.js"
4
+ import { exec } from "../exec.js"
5
5
 
6
6
  export const nssCommandName = "libnss3-tools"
7
7
 
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { UNICODE } from "@jsenv/log"
3
3
 
4
- import { memoize } from "@jsenv/https-local/src/internal/memoize.js"
4
+ import { memoize } from "../memoize.js"
5
5
 
6
6
  const REASON_CHROME_NOT_DETECTED = `Chrome not detected`
7
7
 
@@ -1,14 +1,14 @@
1
1
  // https://ss64.com/osx/security.html
2
2
 
3
3
  import { fileURLToPath } from "node:url"
4
- import { createDetailedMessage } from "@jsenv/logger"
4
+ import { createDetailedMessage, UNICODE } from "@jsenv/log"
5
5
 
6
- import { UNICODE } from "@jsenv/log"
7
- import { exec } from "@jsenv/https-local/src/internal/exec.js"
8
- import { searchCertificateInCommandOutput } from "@jsenv/https-local/src/internal/search_certificate_in_command_output.js"
6
+ import { exec } from "../exec.js"
7
+ import { searchCertificateInCommandOutput } from "../search_certificate_in_command_output.js"
9
8
  import {
10
9
  VERB_CHECK_TRUST,
11
10
  VERB_ADD_TRUST,
11
+ VERB_ENSURE_TRUST,
12
12
  VERB_REMOVE_TRUST,
13
13
  } from "../trust_query.js"
14
14
 
@@ -53,6 +53,35 @@ export const executeTrustQueryOnMacKeychain = async ({
53
53
  certificate,
54
54
  )
55
55
 
56
+ const removeCert = async () => {
57
+ // https://ss64.com/osx/security-delete-cert.html
58
+ const removeTrustedCertCommand = `sudo security delete-certificate -c "${certificateCommonName}"`
59
+ logger.info(`Removing certificate from mac keychain...`)
60
+ logger.info(`${UNICODE.COMMAND} ${removeTrustedCertCommand}`)
61
+ try {
62
+ await exec(removeTrustedCertCommand)
63
+ logger.info(`${UNICODE.OK} certificate removed from mac keychain`)
64
+ return {
65
+ status: "not_trusted",
66
+ reason: REASON_REMOVE_FROM_KEYCHAIN_COMMAND_COMPLETED,
67
+ }
68
+ } catch (e) {
69
+ logger.error(
70
+ createDetailedMessage(
71
+ `${UNICODE.FAILURE} failed to remove certificate from mac keychain`,
72
+ {
73
+ "error stack": e.stack,
74
+ "certificate file url": certificateFileUrl,
75
+ },
76
+ ),
77
+ )
78
+ return {
79
+ status: "not_trusted",
80
+ reason: REASON_REMOVE_FROM_KEYCHAIN_COMMAND_FAILED,
81
+ }
82
+ }
83
+ }
84
+
56
85
  if (!certificateFoundInCommandOutput) {
57
86
  logger.info(`${UNICODE.INFO} certificate not found in mac keychain`)
58
87
  if (verb === VERB_CHECK_TRUST || verb === VERB_REMOVE_TRUST) {
@@ -61,7 +90,13 @@ export const executeTrustQueryOnMacKeychain = async ({
61
90
  reason: REASON_NOT_IN_KEYCHAIN,
62
91
  }
63
92
  }
64
-
93
+ if (verb === VERB_ENSURE_TRUST) {
94
+ // It seems possible for certificate PEM representation to be different
95
+ // in mackeychain and in the one we have written on the filesystem
96
+ // When it happens the certificate is not found but actually exists on mackeychain
97
+ // and must be deleted first
98
+ await removeCert()
99
+ }
65
100
  const certificateFilePath = fileURLToPath(certificateFileUrl)
66
101
  // https://ss64.com/osx/security-cert.html
67
102
  const addTrustedCertCommand = `sudo security add-trusted-cert -d -r trustRoot -k ${systemKeychainPath} "${certificateFilePath}"`
@@ -96,37 +131,16 @@ export const executeTrustQueryOnMacKeychain = async ({
96
131
  // but they shouldn't and I couldn't find an API to know if the cert is trusted or not
97
132
  // just if it's in the keychain
98
133
  logger.info(`${UNICODE.OK} certificate found in mac keychain`)
99
- if (verb === VERB_CHECK_TRUST || verb === VERB_ADD_TRUST) {
134
+ if (
135
+ verb === VERB_CHECK_TRUST ||
136
+ verb === VERB_ADD_TRUST ||
137
+ verb === VERB_ENSURE_TRUST
138
+ ) {
100
139
  return {
101
140
  status: "trusted",
102
141
  reason: REASON_IN_KEYCHAIN,
103
142
  }
104
143
  }
105
144
 
106
- // https://ss64.com/osx/security-delete-cert.html
107
- const removeTrustedCertCommand = `sudo security delete-certificate -c "${certificateCommonName}"`
108
- logger.info(`Removing certificate from mac keychain...`)
109
- logger.info(`${UNICODE.COMMAND} ${removeTrustedCertCommand}`)
110
- try {
111
- await exec(removeTrustedCertCommand)
112
- logger.info(`${UNICODE.OK} certificate removed from mac keychain`)
113
- return {
114
- status: "not_trusted",
115
- reason: REASON_REMOVE_FROM_KEYCHAIN_COMMAND_COMPLETED,
116
- }
117
- } catch (e) {
118
- logger.error(
119
- createDetailedMessage(
120
- `${UNICODE.FAILURE} failed to remove certificate from mac keychain`,
121
- {
122
- "error stack": e.stack,
123
- "certificate file url": certificateFileUrl,
124
- },
125
- ),
126
- )
127
- return {
128
- status: "not_trusted",
129
- reason: REASON_REMOVE_FROM_KEYCHAIN_COMMAND_FAILED,
130
- }
131
- }
145
+ return removeCert()
132
146
  }
@@ -2,9 +2,9 @@ import { fileURLToPath } from "node:url"
2
2
  import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
3
3
  import { UNICODE } from "@jsenv/log"
4
4
 
5
- import { memoize } from "@jsenv/https-local/src/internal/memoize.js"
6
- import { exec } from "@jsenv/https-local/src/internal/exec.js"
7
- import { commandExists } from "@jsenv/https-local/src/internal/command.js"
5
+ import { memoize } from "../memoize.js"
6
+ import { exec } from "../exec.js"
7
+ import { commandExists } from "../command.js"
8
8
 
9
9
  export const nssCommandName = "nss"
10
10
 
@@ -5,14 +5,17 @@
5
5
 
6
6
  import { existsSync } from "node:fs"
7
7
  import { fileURLToPath } from "node:url"
8
- import { createDetailedMessage } from "@jsenv/logger"
9
8
  import { urlToFilename } from "@jsenv/urls"
9
+ import { createDetailedMessage, UNICODE } from "@jsenv/log"
10
10
  import { assertAndNormalizeDirectoryUrl, collectFiles } from "@jsenv/filesystem"
11
- import { UNICODE } from "@jsenv/log"
12
11
 
13
- import { exec } from "@jsenv/https-local/src/internal/exec.js"
14
- import { searchCertificateInCommandOutput } from "@jsenv/https-local/src/internal/search_certificate_in_command_output.js"
15
- import { VERB_CHECK_TRUST, VERB_ADD_TRUST } from "./trust_query.js"
12
+ import { exec } from "./exec.js"
13
+ import { searchCertificateInCommandOutput } from "./search_certificate_in_command_output.js"
14
+ import {
15
+ VERB_CHECK_TRUST,
16
+ VERB_ADD_TRUST,
17
+ VERB_ENSURE_TRUST,
18
+ } from "./trust_query.js"
16
19
 
17
20
  export const executeTrustQueryOnBrowserNSSDB = async ({
18
21
  logger,
@@ -53,7 +56,7 @@ export const executeTrustQueryOnBrowserNSSDB = async ({
53
56
  const nssIsInstalled = await detectIfNSSIsInstalled({ logger })
54
57
  const cannotCheckMessage = `${UNICODE.FAILURE} cannot check if certificate is in ${browserName}`
55
58
  if (!nssIsInstalled) {
56
- if (verb === VERB_ADD_TRUST) {
59
+ if (verb === VERB_ADD_TRUST || verb === VERB_ENSURE_TRUST) {
57
60
  const nssDynamicInstallInfo = await getNSSDynamicInstallInfo({ logger })
58
61
  if (!nssDynamicInstallInfo.isInstallable) {
59
62
  const reason = `"${nssCommandName}" is not installed and not cannot be installed`
@@ -210,7 +213,7 @@ export const executeTrustQueryOnBrowserNSSDB = async ({
210
213
  }
211
214
  }
212
215
 
213
- if (verb === VERB_ADD_TRUST) {
216
+ if (verb === VERB_ADD_TRUST || verb === VERB_ENSURE_TRUST) {
214
217
  if (missingCount === 0 && outdatedCount === 0) {
215
218
  logger.info(`${UNICODE.OK} certificate found in ${browserName}`)
216
219
  return {
@@ -1,5 +1,4 @@
1
1
  export const VERB_CHECK_TRUST = "CHECK_TRUST"
2
-
3
2
  export const VERB_ADD_TRUST = "ADD_TRUST"
4
-
3
+ export const VERB_ENSURE_TRUST = "ENSURE_TRUST"
5
4
  export const VERB_REMOVE_TRUST = "REMOVE_TRUST"
@@ -2,7 +2,7 @@ import { createRequire } from "node:module"
2
2
  import { existsSync } from "node:fs"
3
3
  import { UNICODE } from "@jsenv/log"
4
4
 
5
- import { memoize } from "@jsenv/https-local/src/internal/memoize.js"
5
+ import { memoize } from "../memoize.js"
6
6
 
7
7
  const require = createRequire(import.meta.url)
8
8
 
@@ -7,7 +7,7 @@ import { createRequire } from "node:module"
7
7
  import { existsSync } from "node:fs"
8
8
  import { UNICODE } from "@jsenv/log"
9
9
 
10
- import { memoize } from "@jsenv/https-local/src/internal/memoize.js"
10
+ import { memoize } from "../memoize.js"
11
11
 
12
12
  const require = createRequire(import.meta.url)
13
13
 
@@ -4,13 +4,13 @@
4
4
  */
5
5
 
6
6
  import { fileURLToPath } from "node:url"
7
- import { createDetailedMessage } from "@jsenv/logger"
8
- import { UNICODE } from "@jsenv/log"
7
+ import { createDetailedMessage, UNICODE } from "@jsenv/log"
9
8
 
10
- import { exec } from "@jsenv/https-local/src/internal/exec.js"
9
+ import { exec } from "../exec.js"
11
10
  import {
12
11
  VERB_CHECK_TRUST,
13
12
  VERB_ADD_TRUST,
13
+ VERB_ENSURE_TRUST,
14
14
  VERB_REMOVE_TRUST,
15
15
  } from "../trust_query.js"
16
16
 
@@ -94,7 +94,11 @@ export const executeTrustQueryOnWindows = async ({
94
94
  }
95
95
 
96
96
  logger.info(`${UNICODE.OK} certificate found in windows`)
97
- if (verb === VERB_CHECK_TRUST || verb === VERB_ADD_TRUST) {
97
+ if (
98
+ verb === VERB_CHECK_TRUST ||
99
+ verb === VERB_ADD_TRUST ||
100
+ verb === VERB_ENSURE_TRUST
101
+ ) {
98
102
  return {
99
103
  status: "trusted",
100
104
  reason: REASON_FOUND_IN_WINDOWS,
@@ -7,13 +7,13 @@
7
7
  export {
8
8
  installCertificateAuthority,
9
9
  uninstallCertificateAuthority,
10
- } from "./src/certificate_authority.js"
10
+ } from "./certificate_authority.js"
11
11
 
12
12
  export {
13
13
  createValidityDurationOfXYears,
14
14
  createValidityDurationOfXDays,
15
- } from "./src/validity_duration.js"
15
+ } from "./validity_duration.js"
16
16
 
17
- export { verifyHostsFile } from "./src/hosts_file_verif.js"
17
+ export { verifyHostsFile } from "./hosts_file_verif.js"
18
18
 
19
- export { requestCertificateForLocalhost } from "./src/certificate_for_localhost.js"
19
+ export { requestCertificate } from "./certificate_request.js"
@@ -3,7 +3,6 @@ const MILLISECONDS_PER_YEAR = MILLISECONDS_PER_DAY * 365
3
3
 
4
4
  export const verifyRootCertificateValidityDuration = (validityDurationInMs) => {
5
5
  const durationInYears = validityDurationInMs / MILLISECONDS_PER_YEAR
6
-
7
6
  if (durationInYears > 25) {
8
7
  return {
9
8
  ok: false,
@@ -13,26 +12,22 @@ export const verifyRootCertificateValidityDuration = (validityDurationInMs) => {
13
12
  "https://serverfault.com/questions/847190/in-theory-could-a-ca-make-a-certificate-that-is-valid-for-arbitrarily-long",
14
13
  }
15
14
  }
16
-
17
15
  return { ok: true }
18
16
  }
19
17
 
20
18
  export const verifyServerCertificateValidityDuration = (
21
- serverCertificateValidityDurationInMs,
19
+ validityDurationInMs,
22
20
  ) => {
23
- const serverCertificateValidityDurationInDays =
24
- serverCertificateValidityDurationInMs / MILLISECONDS_PER_DAY
25
-
26
- if (serverCertificateValidityDurationInDays > 397) {
21
+ const validityDurationInDays = validityDurationInMs / MILLISECONDS_PER_DAY
22
+ if (validityDurationInDays > 397) {
27
23
  return {
28
24
  ok: false,
29
25
  maxAllowedValue: MILLISECONDS_PER_DAY * 397,
30
- message: `certificate validity duration of ${serverCertificateValidityDurationInMs} days is too much, using the max recommended duration: 397 days`,
26
+ message: `certificate validity duration of ${validityDurationInMs} days is too much, using the max recommended duration: 397 days`,
31
27
  details:
32
28
  "https://www.globalsign.com/en/blog/maximum-ssltls-certificate-validity-now-one-year",
33
29
  }
34
30
  }
35
-
36
31
  return { ok: true }
37
32
  }
38
33