@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.
@@ -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@v3
26
- - name: Setup Node.js
27
- uses: actions/setup-node@v3
28
+ uses: actions/checkout@v4
28
29
  with:
29
- node-version: 14
30
- - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v3
32
- id: semantic
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
- extra_plugins: |
35
- @semantic-release/changelog
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
- @semantic-release/github
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.GITHUB }}"
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 }}"
@@ -14,8 +14,7 @@ jobs:
14
14
  env:
15
15
  DRY_RUN: ${{ github.event.inputs.dry_run }}
16
16
  GITHUB_TOKEN: "${{ secrets.GITHUB }}"
17
- NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
18
-
17
+
19
18
  steps:
20
19
  - name: Checkout
21
20
  uses: actions/checkout@v3
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.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
- module.exports = {
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/git",
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
- const { Client, forge } = require('acme-client');
2
- const fs = require('fs');
3
- const util = require('node:util');
4
- const exec = util.promisify(require('node:child_process').exec);
5
-
6
- const certificates = {}
7
- const email = 'ssl@cocreate.app';
8
- const keyPath = '/etc/certificates/';
9
- let client
10
- const hosts = {}
11
-
12
- // Run this once per server to generate random constants
13
- const DAYS = Math.floor(Math.random() * 7); // Random days between 0-6
14
- const HOURS = Math.floor(Math.random() * 24); // Random hours between 0-23
15
- const MINUTES = Math.floor(Math.random() * 60); // Random minutes between 0-59
16
-
17
- // Store these constants in a configuration file, environment variable, or database
18
-
19
- class CoCreateAcme {
20
- constructor(proxy, crud) {
21
- this.proxy = proxy
22
- this.crud = crud
23
- // this.check = this.checkCertificate
24
- this.init().catch(err => {
25
- console.error('Error initializing ACME client:', err);
26
- // TODO: Handle initialization error (possibly retry or exit)
27
- });
28
-
29
- }
30
-
31
- async init() {
32
- await exec('sudo mkdir -p /etc/certificates');
33
- await exec('sudo chmod 777 /etc/certificates');
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; // New account, so will need to register it
49
- fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
50
- // fs.chmodSync(accountKeyPath, '400')
58
+ isNewAccount = true;
59
+ fs.writeFileSync(accountKeyPath, accountKey);
51
60
  } else {
52
- accountKey = fs.readFileSync(accountKeyPath, 'utf8');
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: 'https://acme-v02.api.letsencrypt.org/directory', // https://acme-v02.api.letsencrypt.org/directory https://acme-staging-v02.api.letsencrypt.org/directory
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: ['mailto:' + email]
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
- fs.unlinkSync(accountKeyPath)
72
- // Handle errors that occur during account creation
73
- console.error("Error creating ACME account:", error.message);
74
- // Depending on the type of error, you might want to retry, log the error, alert someone, etc.
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
- async requestCertificate(host, hostPosition, hostObject, organization_id, wildcard = false) {
80
- try {
81
-
82
- const self = this
83
-
84
- const hostKeyPath = keyPath + host + '/';
85
- if (!fs.existsSync(hostKeyPath)) {
86
- fs.mkdirSync(hostKeyPath, { recursive: true });
87
- }
88
-
89
- const domains = wildcard ? [host, `*.${host}`] : [host];
90
-
91
- /* Create certificate request */
92
- let [key, csr] = await forge.createCsr({
93
- commonName: domains[0],
94
- altNames: domains
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
- challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
149
- /* Clean up challenge response here if necessary */
150
- console.log(`Challenge removed for token: ${challenge.token}`);
151
- if (challenge.type === 'http-01') {
152
- self.crud.send({
153
- method: 'object.delete',
154
- host,
155
- array: 'files',
156
- object: {
157
- _id: challenge_id
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
- let expires = await forge.readCertificateInfo(cert);
169
- expires = expires.notAfter;
170
- this.setCertificate(host, expires, organization_id, hostKeyPath, cert, key)
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
- key = key.toString('utf8');
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
- this.crud.send({
175
- method: 'object.update',
192
+ if (server && server.crud) {
193
+ server.crud.send({
194
+ method: "object.update",
176
195
  host,
177
- array: 'organizations',
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
- async getCertificate(host, organization_id) {
196
- const hostKeyPath = keyPath + host + '/';
197
- let hostPosition, hostObject
198
-
199
- let organization = await this.crud.getOrganization({ host, organization_id });
200
- if (organization.error)
201
- return false
202
- if (!organization_id)
203
- organization_id = organization._id
204
-
205
- if (organization.host) {
206
- for (let i = 0; i < organization.host.length; i++) {
207
- if (organization.host[i].name === host) {
208
- hostPosition = i
209
- hostObject = organization.host[i]
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
- async checkCertificate(host, organization_id, pathname = '') {
230
- let hostname = host.split(':')[0]
231
- if (hostname === 'localhost' || hostname === '127.0.0.1' || pathname.startsWith('/.well-known/acme-challenge/'))
232
- return true
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
- const hostKeyPath = keyPath + host + '/';
239
- if (fs.existsSync(hostKeyPath + 'fullchain.pem')) {
240
- let cert = fs.readFileSync(hostKeyPath + 'fullchain.pem', 'utf8');
241
- let expires = await forge.readCertificateInfo(cert);
242
- expires = expires.notAfter;
243
- if (this.isValid(expires)) {
244
- this.setCertificate(host, expires, organization_id)
245
- return true
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
- isValid(expires) {
255
- let currentDate = new Date();
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
- if (expires && currentDate < expires) {
261
- return true; // SSL is still valid, no need to renew
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
- setCertificate(host, expires, organization_id, hostKeyPath, cert, key) {
266
- // let expireDate = new Date(expires);
267
- // let currentDate = new Date();
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
- this.proxy.createServer(host)
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
- certificates[host] = { expires, organization_id }
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
- module.exports = CoCreateAcme;
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
+ };