@cocreate/acme 1.3.1 → 1.4.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/.github/workflows/automated.yml +25 -13
- package/.github/workflows/manual.yml +1 -2
- package/CHANGELOG.md +30 -0
- package/package.json +3 -3
- package/release.config.js +12 -4
- package/src/index.js +271 -256
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
name: Automated Workflow
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
branches:
|
|
5
6
|
- master
|
|
7
|
+
|
|
6
8
|
jobs:
|
|
7
9
|
about:
|
|
8
10
|
runs-on: ubuntu-latest
|
|
@@ -18,27 +20,37 @@ jobs:
|
|
|
18
20
|
with:
|
|
19
21
|
direction: overwrite-github
|
|
20
22
|
githubToken: "${{ secrets.GITHUB }}"
|
|
23
|
+
|
|
21
24
|
release:
|
|
22
25
|
runs-on: ubuntu-latest
|
|
23
26
|
steps:
|
|
24
27
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
26
|
-
- name: Setup Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
+
uses: actions/checkout@v4
|
|
28
29
|
with:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
fetch-depth: 0 # Required so semantic-release can trace git tags/history
|
|
31
|
+
|
|
32
|
+
- name: Setup Node.js
|
|
33
|
+
uses: actions/setup-node@v4
|
|
33
34
|
with:
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
node-version: 22
|
|
36
|
+
|
|
37
|
+
- name: Install Semantic Release & Plugins
|
|
38
|
+
# Installs semantic-release and its plugins on the runner
|
|
39
|
+
run: |
|
|
40
|
+
npm install -g semantic-release \
|
|
41
|
+
@semantic-release/changelog \
|
|
42
|
+
@semantic-release/npm \
|
|
43
|
+
@semantic-release/github \
|
|
36
44
|
@semantic-release/git
|
|
37
|
-
|
|
45
|
+
|
|
46
|
+
- name: Run Semantic Release (Native)
|
|
47
|
+
id: semantic
|
|
48
|
+
# This will automatically pick up your export default config file in the repository root
|
|
49
|
+
run: npx semantic-release
|
|
38
50
|
env:
|
|
39
|
-
GITHUB_TOKEN: "${{ secrets.
|
|
51
|
+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
40
52
|
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
|
53
|
+
|
|
41
54
|
outputs:
|
|
42
55
|
new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
|
|
43
|
-
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
|
44
|
-
|
|
56
|
+
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.3...v1.4.0) (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* improve error handling and remove redundant server reference assignments in init function ([69831fe](https://github.com/CoCreate-app/CoCreate-acme/commit/69831fee351f103e60579b34bb915fbed655b3a1))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* update automated workflow and release configuration for improved semantic release handling ([90145c1](https://github.com/CoCreate-app/CoCreate-acme/commit/90145c12f6421d03cbb66d8f4b7c29891b1d2ef0))
|
|
12
|
+
|
|
13
|
+
## [1.3.3](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.2...v1.3.3) (2026-07-14)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* removed post install ([cc2012f](https://github.com/CoCreate-app/CoCreate-acme/commit/cc2012f8ada625665074ecbaad14b895358a3ae7))
|
|
19
|
+
* semantic version handling. Reorganize .gitignore for improved clarity and structure ([6e69e47](https://github.com/CoCreate-app/CoCreate-acme/commit/6e69e47e1643255a623bee33279320d0a825ce4f))
|
|
20
|
+
* update module export to ES6 syntax in release.config.js ([5833855](https://github.com/CoCreate-app/CoCreate-acme/commit/58338552088e727bb7abbeb77107dcb87f82bfa5))
|
|
21
|
+
* update package.json to use ESM and restructure server/client entry points ([6eb6796](https://github.com/CoCreate-app/CoCreate-acme/commit/6eb679673260922698716bf5e5e35a5044a1dbd1))
|
|
22
|
+
|
|
23
|
+
## [1.3.2](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.1...v1.3.2) (2025-09-06)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* await async function calls ([cf151f1](https://github.com/CoCreate-app/CoCreate-acme/commit/cf151f186b4893411f273a6a3e3f08b87093b72c))
|
|
29
|
+
* formating ([bcce249](https://github.com/CoCreate-app/CoCreate-acme/commit/bcce24906478e43aa4b1845c9f9099623551128e))
|
|
30
|
+
|
|
1
31
|
## [1.3.1](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.0...v1.3.1) (2025-05-01)
|
|
2
32
|
|
|
3
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/acme",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Dynamic SSL certificate management leveraging the ACME protocol, designed for direct API accessibility within applications. Automates certificate creation, renewal, and retrieval, ideal for distributed applications requiring real-time, secure certificate operations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"acme",
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "npx webpack --config webpack.config.js",
|
|
24
24
|
"build": "npx webpack --mode=production --config webpack.config.js",
|
|
25
|
-
"dev": "npx webpack --config webpack.config.js --watch"
|
|
26
|
-
"postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
|
|
25
|
+
"dev": "npx webpack --config webpack.config.js --watch"
|
|
27
26
|
},
|
|
28
27
|
"repository": {
|
|
29
28
|
"type": "git",
|
|
@@ -39,6 +38,7 @@
|
|
|
39
38
|
"type": "GitHub Sponsors ❤",
|
|
40
39
|
"url": "https://github.com/sponsors/CoCreate-app"
|
|
41
40
|
},
|
|
41
|
+
"type": "module",
|
|
42
42
|
"main": "./src/index.js",
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"acme-client": "^5.0.0"
|
package/release.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default {
|
|
2
2
|
dryRun: false,
|
|
3
3
|
branches: ["master"],
|
|
4
4
|
plugins: [
|
|
@@ -10,12 +10,20 @@ module.exports = {
|
|
|
10
10
|
changelogFile: "CHANGELOG.md",
|
|
11
11
|
},
|
|
12
12
|
],
|
|
13
|
-
"@semantic-release/npm",
|
|
14
|
-
"@semantic-release/github",
|
|
13
|
+
"@semantic-release/npm",
|
|
15
14
|
[
|
|
16
|
-
"@semantic-release/
|
|
15
|
+
"@semantic-release/github",
|
|
17
16
|
{
|
|
17
|
+
successComment: false,
|
|
18
|
+
failTitle: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"@semantic-release/git",
|
|
23
|
+
{
|
|
24
|
+
// Only stage and commit the changelog and package.json
|
|
18
25
|
assets: ["CHANGELOG.md", "package.json"],
|
|
26
|
+
message: "chore(release): ${nextRelease.version} [skip ci]",
|
|
19
27
|
},
|
|
20
28
|
],
|
|
21
29
|
],
|
package/src/index.js
CHANGED
|
@@ -1,310 +1,325 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
|
|
18
|
+
import { Client, forge } from "acme-client";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import tls from "tls";
|
|
21
|
+
import { promisify } from "node:util";
|
|
22
|
+
import { exec as childExec } from "node:child_process";
|
|
23
|
+
|
|
24
|
+
const exec = promisify(childExec);
|
|
25
|
+
|
|
26
|
+
// ==========================================
|
|
27
|
+
// Module-Level Sandbox State (ESM Singleton Container)
|
|
28
|
+
// ==========================================
|
|
29
|
+
let server = null; // Direct reference used across all module functions
|
|
30
|
+
const certificates = {};
|
|
31
|
+
const email = "ssl@cocreate.app";
|
|
32
|
+
const keyPath = "/etc/certificates/";
|
|
33
|
+
let client;
|
|
34
|
+
const hosts = {};
|
|
35
|
+
|
|
36
|
+
const DAYS = Math.floor(Math.random() * 7);
|
|
37
|
+
const HOURS = Math.floor(Math.random() * 24);
|
|
38
|
+
const MINUTES = Math.floor(Math.random() * 60);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bootstraps the ACME account keys, directories, and instantiates the
|
|
42
|
+
* secure HTTPS server context, attaching it back to the parent server reference.
|
|
43
|
+
* @param {Object} parentServer - Parent CoCreateServer singleton instance
|
|
44
|
+
*/
|
|
45
|
+
export async function init(parentServer) {
|
|
46
|
+
try {
|
|
47
|
+
server = parentServer;
|
|
48
|
+
|
|
49
|
+
await exec("sudo mkdir -p /etc/certificates");
|
|
50
|
+
await exec("sudo chmod 777 /etc/certificates");
|
|
51
|
+
|
|
52
|
+
const accountKeyPath = `${keyPath}account.pem`;
|
|
53
|
+
let accountKey = "";
|
|
54
|
+
let isNewAccount = false;
|
|
34
55
|
|
|
35
|
-
// if (!fs.existsSync(keyPath)) {
|
|
36
|
-
// fs.mkdirSync(keyPath, { recursive: true }); // Create the directory if it doesn't exist
|
|
37
|
-
// }
|
|
38
|
-
|
|
39
|
-
const accountKeyPath = keyPath + 'account.pem';
|
|
40
|
-
|
|
41
|
-
let accountKey = '';
|
|
42
|
-
let isNewAccount = false; // Flag to check if the account is new
|
|
43
|
-
|
|
44
|
-
// Check if the account key exists and load it; otherwise, create a new one
|
|
45
56
|
if (!fs.existsSync(accountKeyPath)) {
|
|
46
|
-
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
|
|
47
57
|
accountKey = await forge.createPrivateKey();
|
|
48
|
-
isNewAccount = true;
|
|
49
|
-
fs.writeFileSync(accountKeyPath, accountKey);
|
|
50
|
-
// fs.chmodSync(accountKeyPath, '400')
|
|
58
|
+
isNewAccount = true;
|
|
59
|
+
fs.writeFileSync(accountKeyPath, accountKey);
|
|
51
60
|
} else {
|
|
52
|
-
accountKey = fs.readFileSync(accountKeyPath,
|
|
61
|
+
accountKey = fs.readFileSync(accountKeyPath, "utf8");
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
// Initialize the ACME client with the account key
|
|
56
64
|
client = new Client({
|
|
57
|
-
directoryUrl:
|
|
65
|
+
directoryUrl: "https://acme-v02.api.letsencrypt.org/directory",
|
|
58
66
|
accountKey: accountKey
|
|
59
67
|
});
|
|
60
68
|
|
|
61
|
-
// Register the new account if it was just created
|
|
62
69
|
if (isNewAccount) {
|
|
63
70
|
try {
|
|
64
|
-
// Attempt to create an account
|
|
65
71
|
await client.createAccount({
|
|
66
72
|
termsOfServiceAgreed: true,
|
|
67
|
-
contact: [
|
|
73
|
+
contact: [`mailto:${email}`]
|
|
68
74
|
});
|
|
69
|
-
console.log("ACME account created successfully!");
|
|
75
|
+
console.log("[@cocreate/acme] ACME authority account created successfully!");
|
|
70
76
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
try {
|
|
78
|
+
fs.unlinkSync(accountKeyPath);
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
console.error("[@cocreate/acme] Error creating account credentials:", error.message);
|
|
75
81
|
}
|
|
76
82
|
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("[@cocreate/acme] Error initializing certificate client:", err);
|
|
87
|
+
throw err;
|
|
77
88
|
}
|
|
89
|
+
}
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let challenge_id = ''
|
|
98
|
-
|
|
99
|
-
/* Request certificate */
|
|
100
|
-
const cert = await client.auto({
|
|
101
|
-
csr,
|
|
102
|
-
email: [email], // Replace with your email
|
|
103
|
-
termsOfServiceAgreed: true,
|
|
104
|
-
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
|
105
|
-
console.log(`Challenge added for host: ${host} token: ${challenge.token}`);
|
|
106
|
-
|
|
107
|
-
if (challenge.type === 'http-01') {
|
|
108
|
-
const httpChallenge = await self.crud.send({
|
|
109
|
-
method: 'object.create',
|
|
110
|
-
host,
|
|
111
|
-
array: 'files',
|
|
112
|
-
object: {
|
|
113
|
-
"content-type": "text/plain",
|
|
114
|
-
"directory": "acme-challenge",
|
|
115
|
-
"host": [
|
|
116
|
-
authz.identifier.value
|
|
117
|
-
],
|
|
118
|
-
"name": challenge.token,
|
|
119
|
-
"organization_id": "652c8d62679eca03e0b116a7",
|
|
120
|
-
"path": "/.well-known/acme-challenge/",
|
|
121
|
-
"pathname": `/.well-known/acme-challenge/${challenge.token}`,
|
|
122
|
-
"public": "true",
|
|
123
|
-
"src": keyAuthorization
|
|
124
|
-
},
|
|
125
|
-
organization_id,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (httpChallenge && httpChallenge.object && httpChallenge.object[0])
|
|
129
|
-
challenge_id = httpChallenge.object[0]._id
|
|
130
|
-
else
|
|
131
|
-
console.error('error creating challenge url')
|
|
132
|
-
|
|
133
|
-
} else if (challenge.type === 'dns-01') {
|
|
134
|
-
// Calculate the DNS TXT record value
|
|
135
|
-
const dnsRecordName = `_acme-challenge.${authz.identifier.value}`;
|
|
136
|
-
const dnsRecordValue = await client.getChallengeKeyAuthorization(challenge);
|
|
137
|
-
|
|
138
|
-
console.log(`Add this TXT record to your DNS:`);
|
|
139
|
-
console.log(`Name: ${dnsRecordName}`);
|
|
140
|
-
console.log(`Value: ${dnsRecordValue}`);
|
|
141
|
-
|
|
142
|
-
// Here, implement the logic to add the TXT record to your DNS
|
|
143
|
-
// await updateDnsTxtRecord(dnsRecordName, dnsRecordValue); // Hypothetical function to update DNS
|
|
144
|
-
}
|
|
91
|
+
/**
|
|
92
|
+
* Interface Contract:
|
|
93
|
+
* Reads SSL credentials dynamically from the secure file system.
|
|
94
|
+
* @param {string} domain - Target host address
|
|
95
|
+
* @returns {{key: Buffer, cert: Buffer}} Native Node TLS credential buffer
|
|
96
|
+
*/
|
|
97
|
+
export function load(domain) {
|
|
98
|
+
try {
|
|
99
|
+
return {
|
|
100
|
+
key: fs.readFileSync(`${keyPath}${domain}/private-key.pem`),
|
|
101
|
+
cert: fs.readFileSync(`${keyPath}${domain}/fullchain.pem`),
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`[@cocreate/acme] Error loading SSL certificates for domain: ${domain}`, error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
145
108
|
|
|
146
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Core SNI Handshake implementation. Resolves domains dynamically and loads TLS Context.
|
|
111
|
+
* @param {string} domain - Incoming target SNI hostname
|
|
112
|
+
* @param {Function} cb - Resolution completion callback
|
|
113
|
+
*/
|
|
114
|
+
export async function sniCallback(domain, cb) {
|
|
115
|
+
try {
|
|
116
|
+
// Verify domain authorization
|
|
117
|
+
const isValidDomain = await check(domain);
|
|
118
|
+
|
|
119
|
+
if (isValidDomain) {
|
|
120
|
+
const credentials = load(domain);
|
|
121
|
+
const sslContext = tls.createSecureContext(credentials);
|
|
122
|
+
cb(null, sslContext);
|
|
123
|
+
} else {
|
|
124
|
+
cb(new Error(`[@cocreate/acme] Unauthorized secure request for domain: ${domain}`));
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`[@cocreate/acme] Error in SNI callback for domain: ${domain}`, error);
|
|
128
|
+
cb(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
147
131
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
159
|
-
organization_id,
|
|
160
|
-
});
|
|
161
|
-
} else if (challenge.type === 'dns-01') {
|
|
162
|
-
// await removeDnsTxtRecord(challenge); // A hypothetical function to clean up DNS
|
|
163
|
-
}
|
|
132
|
+
/**
|
|
133
|
+
* Requests certificate generation dynamically via ACME protocols.
|
|
134
|
+
*/
|
|
135
|
+
export async function request(host, hostPosition, hostObject, organization_id, wildcard = false) {
|
|
136
|
+
try {
|
|
137
|
+
const hostKeyPath = `${keyPath}${host}/`;
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(hostKeyPath)) {
|
|
140
|
+
fs.mkdirSync(hostKeyPath, { recursive: true });
|
|
141
|
+
}
|
|
164
142
|
|
|
143
|
+
const domains = wildcard ? [host, `*.${host}`] : [host];
|
|
144
|
+
let [key, csr] = await forge.createCsr({ commonName: domains[0], altNames: domains });
|
|
145
|
+
let challenge_id = "";
|
|
146
|
+
|
|
147
|
+
const cert = await client.auto({
|
|
148
|
+
csr,
|
|
149
|
+
email: [email],
|
|
150
|
+
termsOfServiceAgreed: true,
|
|
151
|
+
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
|
152
|
+
if (challenge.type === "http-01" && server && server.crud) {
|
|
153
|
+
const httpChallenge = await server.crud.send({
|
|
154
|
+
method: "object.create",
|
|
155
|
+
host,
|
|
156
|
+
array: "files",
|
|
157
|
+
object: {
|
|
158
|
+
"content-type": "text/plain",
|
|
159
|
+
directory: "acme-challenge",
|
|
160
|
+
host: [authz.identifier.value],
|
|
161
|
+
name: challenge.token,
|
|
162
|
+
organization_id: "652c8d62679eca03e0b116a7",
|
|
163
|
+
path: "/.well-known/acme-challenge/",
|
|
164
|
+
pathname: `/.well-known/acme-challenge/${challenge.token}`,
|
|
165
|
+
public: "true",
|
|
166
|
+
src: keyAuthorization
|
|
167
|
+
},
|
|
168
|
+
organization_id
|
|
169
|
+
});
|
|
170
|
+
if (httpChallenge?.object?.[0]) challenge_id = httpChallenge.object[0]._id;
|
|
165
171
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
},
|
|
173
|
+
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
|
174
|
+
if (challenge.type === "http-01" && server && server.crud) {
|
|
175
|
+
server.crud.send({
|
|
176
|
+
method: "object.delete",
|
|
177
|
+
host,
|
|
178
|
+
array: "files",
|
|
179
|
+
object: { _id: challenge_id },
|
|
180
|
+
organization_id
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
171
185
|
|
|
172
|
-
|
|
186
|
+
let expires = await forge.readCertificateInfo(cert);
|
|
187
|
+
expires = expires.notAfter;
|
|
188
|
+
|
|
189
|
+
set(host, expires, organization_id, hostKeyPath, cert, key);
|
|
190
|
+
key = key.toString("utf8");
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
|
|
192
|
+
if (server && server.crud) {
|
|
193
|
+
server.crud.send({
|
|
194
|
+
method: "object.update",
|
|
176
195
|
host,
|
|
177
|
-
array:
|
|
196
|
+
array: "organizations",
|
|
178
197
|
object: {
|
|
179
198
|
_id: organization_id,
|
|
180
|
-
[`host[${hostPosition}]`]: { ...hostObject, name: host, cert, key, expires }
|
|
199
|
+
[`host[${hostPosition}]`]: { ...hostObject, name: host, cert, key, expires }
|
|
181
200
|
},
|
|
182
201
|
organization_id
|
|
183
202
|
});
|
|
184
|
-
|
|
185
|
-
console.log(`Certificate successfully created for ${host}!'`);
|
|
186
|
-
return true
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.log(`Certificate failed to create for ${host}!`);
|
|
189
|
-
delete hosts[host]
|
|
190
|
-
return false
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
return true;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(`[@cocreate/acme] Certificate failed to create for ${host}!`, error);
|
|
208
|
+
delete hosts[host];
|
|
209
|
+
return false;
|
|
193
210
|
}
|
|
211
|
+
}
|
|
194
212
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (organization.host[i].cert && organization.host[i].key) {
|
|
211
|
-
let expires = await forge.readCertificateInfo(organization.host[i].cert);
|
|
212
|
-
expires = expires.notAfter;
|
|
213
|
-
if (this.isValid(expires)) {
|
|
214
|
-
this.setCertificate(host, expires, organization_id, hostKeyPath, organization.host[i].cert, organization.host[i].key)
|
|
215
|
-
return true
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
break
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (!hostPosition && hostPosition !== 0)
|
|
223
|
-
return false
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return await this.requestCertificate(host, hostPosition, hostObject, organization_id, false)
|
|
213
|
+
/**
|
|
214
|
+
* Resolves organization records and matches valid certificate credentials.
|
|
215
|
+
*/
|
|
216
|
+
export async function get(host, organization_id) {
|
|
217
|
+
const hostKeyPath = `${keyPath}${host}/`;
|
|
218
|
+
let hostPosition, hostObject;
|
|
219
|
+
|
|
220
|
+
if (!server || !server.crud) return false;
|
|
221
|
+
|
|
222
|
+
let organization;
|
|
223
|
+
try {
|
|
224
|
+
organization = await server.crud.getOrganization({ host, organization_id });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(`[@cocreate/acme] Failed database lookup during organization query for ${host}:`, error.message);
|
|
227
|
+
return false;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (certificates[host]) {
|
|
235
|
-
return true
|
|
236
|
-
}
|
|
230
|
+
// Safety check: Prevent TypeErrors if the DB yields undefined or returns an explicit error
|
|
231
|
+
if (!organization || organization.error) {
|
|
232
|
+
console.warn(`[@cocreate/acme] Organization query for host "${host}" returned empty or error state.`);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
237
235
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
236
|
+
if (organization.host) {
|
|
237
|
+
for (let i = 0; i < organization.host.length; i++) {
|
|
238
|
+
if (organization.host[i].name === host) {
|
|
239
|
+
hostPosition = i;
|
|
240
|
+
hostObject = organization.host[i];
|
|
241
|
+
|
|
242
|
+
if (organization.host[i].cert && organization.host[i].key) {
|
|
243
|
+
let expires = await forge.readCertificateInfo(organization.host[i].cert);
|
|
244
|
+
expires = expires.notAfter;
|
|
245
|
+
|
|
246
|
+
if (isValid(expires)) {
|
|
247
|
+
set(host, expires, organization_id, hostKeyPath, organization.host[i].cert, organization.host[i].key);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
246
252
|
}
|
|
247
253
|
}
|
|
248
|
-
|
|
249
|
-
if (!hosts[host])
|
|
250
|
-
hosts[host] = this.getCertificate(host, organization_id)
|
|
251
|
-
return hosts[host]
|
|
254
|
+
if (!hostPosition && hostPosition !== 0) return false;
|
|
252
255
|
}
|
|
253
256
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
currentDate.setDate(currentDate.getDate() + DAYS);
|
|
257
|
-
currentDate.setHours(currentDate.getHours() + HOURS);
|
|
258
|
-
currentDate.setMinutes(currentDate.getMinutes() + MINUTES);
|
|
257
|
+
return await request(host, hostPosition, hostObject, organization_id, false);
|
|
258
|
+
}
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Checks database registries or local systems to verify if a valid cert exists.
|
|
262
|
+
*/
|
|
263
|
+
export async function check(host, organization_id, pathname = "") {
|
|
264
|
+
let hostname = host.split(":")[0];
|
|
265
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || pathname.startsWith("/.well-known/acme-challenge/")) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (certificates[host]) return true;
|
|
269
|
+
|
|
270
|
+
const hostKeyPath = `${keyPath}${host}/`;
|
|
271
|
+
if (fs.existsSync(`${hostKeyPath}fullchain.pem`)) {
|
|
272
|
+
let cert = fs.readFileSync(`${hostKeyPath}fullchain.pem`, "utf8");
|
|
273
|
+
let expires = await forge.readCertificateInfo(cert);
|
|
274
|
+
expires = expires.notAfter;
|
|
275
|
+
|
|
276
|
+
if (isValid(expires)) {
|
|
277
|
+
set(host, expires, organization_id);
|
|
278
|
+
return true;
|
|
262
279
|
}
|
|
263
280
|
}
|
|
264
281
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Adjust the expireDate by the DAYS, HOURS, and MINUTES constants
|
|
270
|
-
// expireDate.setDate(expireDate.getDate() - DAYS); // Subtracting to renew earlier
|
|
271
|
-
// expireDate.setHours(expireDate.getHours() - HOURS);
|
|
272
|
-
// expireDate.setMinutes(expireDate.getMinutes() - MINUTES);
|
|
273
|
-
|
|
274
|
-
// Calculate the time difference in milliseconds
|
|
275
|
-
// let timeoutDuration = expireDate.getTime() - currentDate.getTime();
|
|
276
|
-
|
|
277
|
-
// Ensure we're not setting a negative timeout in case of past dates or errors
|
|
278
|
-
// timeoutDuration = Math.max(timeoutDuration, 0);
|
|
279
|
-
|
|
280
|
-
// Clear any existing timeout for the host
|
|
281
|
-
// if (certificates[host] && certificates[host].timeout) {
|
|
282
|
-
// clearTimeout(certificates[host].timeout);
|
|
283
|
-
// }
|
|
284
|
-
|
|
285
|
-
// Set the timeout to call checkCertificate before the actual expiration
|
|
286
|
-
// let timeout = setTimeout(() => {
|
|
287
|
-
// this.checkCertificate(host, organization_id);
|
|
288
|
-
// }, timeoutDuration);
|
|
289
|
-
|
|
290
|
-
// Store the timeout and organization_id for later reference or cancellation
|
|
291
|
-
|
|
292
|
-
if (hostKeyPath) {
|
|
293
|
-
if (!fs.existsSync(hostKeyPath)) {
|
|
294
|
-
fs.mkdirSync(hostKeyPath, { recursive: true });
|
|
295
|
-
}
|
|
296
|
-
fs.writeFileSync(hostKeyPath + 'fullchain.pem', cert);
|
|
297
|
-
// fs.chmodSync(keyPath + 'fullchain.pem', '444')
|
|
298
|
-
fs.writeFileSync(hostKeyPath + 'private-key.pem', key);
|
|
299
|
-
// fs.chmodSync(keyPath + 'private-key.pem', '400')
|
|
300
|
-
}
|
|
282
|
+
if (!hosts[host]) hosts[host] = await get(host, organization_id);
|
|
283
|
+
return hosts[host];
|
|
284
|
+
}
|
|
301
285
|
|
|
302
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Validates if a certificate has expired based on standard and randomized drift margins.
|
|
288
|
+
*/
|
|
289
|
+
export function isValid(expires) {
|
|
290
|
+
let currentDate = new Date();
|
|
291
|
+
currentDate.setDate(currentDate.getDate() + DAYS);
|
|
292
|
+
currentDate.setHours(currentDate.getHours() + HOURS);
|
|
293
|
+
currentDate.setMinutes(currentDate.getMinutes() + MINUTES);
|
|
303
294
|
|
|
304
|
-
|
|
295
|
+
return (expires && currentDate < expires);
|
|
296
|
+
}
|
|
305
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Writes dynamic credentials to disk and triggers an update on the host ingress config if applicable.
|
|
300
|
+
*/
|
|
301
|
+
export function set(host, expires, organization_id, hostKeyPath, cert, key) {
|
|
302
|
+
if (hostKeyPath) {
|
|
303
|
+
if (!fs.existsSync(hostKeyPath)) fs.mkdirSync(hostKeyPath, { recursive: true });
|
|
304
|
+
fs.writeFileSync(`${hostKeyPath}fullchain.pem`, cert);
|
|
305
|
+
fs.writeFileSync(`${hostKeyPath}private-key.pem`, key);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Safely bypassed if Nginx is entirely removed/omitted from this deployment
|
|
309
|
+
if (server && server.ingress && typeof server.ingress.createServer === "function") {
|
|
310
|
+
server.ingress.createServer(host);
|
|
311
|
+
}
|
|
312
|
+
certificates[host] = { expires, organization_id };
|
|
308
313
|
}
|
|
309
314
|
|
|
310
|
-
|
|
315
|
+
// Default export mimics the functional API entry point
|
|
316
|
+
export default {
|
|
317
|
+
init,
|
|
318
|
+
load,
|
|
319
|
+
sniCallback,
|
|
320
|
+
request,
|
|
321
|
+
get,
|
|
322
|
+
check,
|
|
323
|
+
isValid,
|
|
324
|
+
set
|
|
325
|
+
};
|