@cocreate/acme 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.4.0...v1.4.1) (2026-07-17)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * bump dependencies ([a6c9134](https://github.com/CoCreate-app/CoCreate-acme/commit/a6c9134248057e7c99472f7c59f1a6b5db961784))
7
+ * update acme-client dependency version to ^5.4.0 ([31be66c](https://github.com/CoCreate-app/CoCreate-acme/commit/31be66c2cfecb9fb82efd8e99d0e076fc4a178d5))
8
+
9
+ # [1.4.0](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.3...v1.4.0) (2026-07-17)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * improve error handling and remove redundant server reference assignments in init function ([69831fe](https://github.com/CoCreate-app/CoCreate-acme/commit/69831fee351f103e60579b34bb915fbed655b3a1))
15
+
16
+
17
+ ### Features
18
+
19
+ * update automated workflow and release configuration for improved semantic release handling ([90145c1](https://github.com/CoCreate-app/CoCreate-acme/commit/90145c12f6421d03cbb66d8f4b7c29891b1d2ef0))
20
+
21
+ ## [1.3.3](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.2...v1.3.3) (2026-07-14)
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * removed post install ([cc2012f](https://github.com/CoCreate-app/CoCreate-acme/commit/cc2012f8ada625665074ecbaad14b895358a3ae7))
27
+ * semantic version handling. Reorganize .gitignore for improved clarity and structure ([6e69e47](https://github.com/CoCreate-app/CoCreate-acme/commit/6e69e47e1643255a623bee33279320d0a825ce4f))
28
+ * update module export to ES6 syntax in release.config.js ([5833855](https://github.com/CoCreate-app/CoCreate-acme/commit/58338552088e727bb7abbeb77107dcb87f82bfa5))
29
+ * update package.json to use ESM and restructure server/client entry points ([6eb6796](https://github.com/CoCreate-app/CoCreate-acme/commit/6eb679673260922698716bf5e5e35a5044a1dbd1))
30
+
1
31
  ## [1.3.2](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.3.1...v1.3.2) (2025-09-06)
2
32
 
3
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/acme",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
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",
@@ -20,10 +20,9 @@
20
20
  "access": "public"
21
21
  },
22
22
  "scripts": {
23
- "start": "npx webpack --config webpack.config.js",
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); } }\""
23
+ "start": "webpack --config webpack.config.js",
24
+ "build": "webpack --mode=production --config webpack.config.js",
25
+ "dev": "webpack --config webpack.config.js --watch"
27
26
  },
28
27
  "repository": {
29
28
  "type": "git",
@@ -39,8 +38,9 @@
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
- "acme-client": "^5.0.0"
44
+ "acme-client": "^5.4.0"
45
45
  }
46
46
  }
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,356 +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
-
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
6
30
  const certificates = {};
7
31
  const email = "ssl@cocreate.app";
8
32
  const keyPath = "/etc/certificates/";
9
33
  let client;
10
34
  const hosts = {};
11
35
 
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
- async init() {
31
- await exec("sudo mkdir -p /etc/certificates");
32
- await exec("sudo chmod 777 /etc/certificates");
33
-
34
- // if (!fs.existsSync(keyPath)) {
35
- // fs.mkdirSync(keyPath, { recursive: true }); // Create the directory if it doesn't exist
36
- // }
37
-
38
- const accountKeyPath = keyPath + "account.pem";
39
-
40
- let accountKey = "";
41
- let isNewAccount = false; // Flag to check if the account is new
42
-
43
- // Check if the account key exists and load it; otherwise, create a new one
44
- if (!fs.existsSync(accountKeyPath)) {
45
- fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
46
- accountKey = await forge.createPrivateKey();
47
- isNewAccount = true; // New account, so will need to register it
48
- fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
49
- // fs.chmodSync(accountKeyPath, '400')
50
- } else {
51
- accountKey = fs.readFileSync(accountKeyPath, "utf8");
52
- }
53
-
54
- // Initialize the ACME client with the account key
55
- client = new Client({
56
- directoryUrl: "https://acme-v02.api.letsencrypt.org/directory", // https://acme-v02.api.letsencrypt.org/directory https://acme-staging-v02.api.letsencrypt.org/directory
57
- accountKey: accountKey
58
- });
59
-
60
- // Register the new account if it was just created
61
- if (isNewAccount) {
62
- try {
63
- // Attempt to create an account
64
- await client.createAccount({
65
- termsOfServiceAgreed: true,
66
- contact: ["mailto:" + email]
67
- });
68
- console.log("ACME account created successfully!");
69
- } catch (error) {
70
- fs.unlinkSync(accountKeyPath);
71
- // Handle errors that occur during account creation
72
- console.error("Error creating ACME account:", error.message);
73
- // Depending on the type of error, you might want to retry, log the error, alert someone, etc.
74
- }
75
- }
76
- }
77
-
78
- async requestCertificate(
79
- host,
80
- hostPosition,
81
- hostObject,
82
- organization_id,
83
- wildcard = false
84
- ) {
85
- try {
86
- const self = this;
87
-
88
- const hostKeyPath = keyPath + host + "/";
89
- if (!fs.existsSync(hostKeyPath)) {
90
- fs.mkdirSync(hostKeyPath, { recursive: true });
91
- }
92
-
93
- const domains = wildcard ? [host, `*.${host}`] : [host];
94
-
95
- /* Create certificate request */
96
- let [key, csr] = await forge.createCsr({
97
- commonName: domains[0],
98
- altNames: domains
99
- });
100
-
101
- let challenge_id = "";
102
-
103
- /* Request certificate */
104
- const cert = await client.auto({
105
- csr,
106
- email: [email], // Replace with your email
107
- termsOfServiceAgreed: true,
108
- challengeCreateFn: async (
109
- authz,
110
- challenge,
111
- keyAuthorization
112
- ) => {
113
- console.log(
114
- `Challenge added for host: ${host} token: ${challenge.token}`
115
- );
116
-
117
- if (challenge.type === "http-01") {
118
- const httpChallenge = await self.crud.send({
119
- method: "object.create",
120
- host,
121
- array: "files",
122
- object: {
123
- "content-type": "text/plain",
124
- directory: "acme-challenge",
125
- host: [authz.identifier.value],
126
- name: challenge.token,
127
- organization_id: "652c8d62679eca03e0b116a7",
128
- path: "/.well-known/acme-challenge/",
129
- pathname: `/.well-known/acme-challenge/${challenge.token}`,
130
- public: "true",
131
- src: keyAuthorization
132
- },
133
- organization_id
134
- });
135
-
136
- if (
137
- httpChallenge &&
138
- httpChallenge.object &&
139
- httpChallenge.object[0]
140
- )
141
- challenge_id = httpChallenge.object[0]._id;
142
- else console.error("error creating challenge url");
143
- } else if (challenge.type === "dns-01") {
144
- // Calculate the DNS TXT record value
145
- const dnsRecordName = `_acme-challenge.${authz.identifier.value}`;
146
- const dnsRecordValue =
147
- await client.getChallengeKeyAuthorization(
148
- challenge
149
- );
150
-
151
- console.log(`Add this TXT record to your DNS:`);
152
- console.log(`Name: ${dnsRecordName}`);
153
- console.log(`Value: ${dnsRecordValue}`);
154
-
155
- // Here, implement the logic to add the TXT record to your DNS
156
- // await updateDnsTxtRecord(dnsRecordName, dnsRecordValue); // Hypothetical function to update DNS
157
- }
158
- },
159
-
160
- challengeRemoveFn: async (
161
- authz,
162
- challenge,
163
- keyAuthorization
164
- ) => {
165
- /* Clean up challenge response here if necessary */
166
- console.log(
167
- `Challenge removed for token: ${challenge.token}`
168
- );
169
- if (challenge.type === "http-01") {
170
- self.crud.send({
171
- method: "object.delete",
172
- host,
173
- array: "files",
174
- object: {
175
- _id: challenge_id
176
- },
177
- organization_id
178
- });
179
- } else if (challenge.type === "dns-01") {
180
- // await removeDnsTxtRecord(challenge); // A hypothetical function to clean up DNS
181
- }
182
- }
183
- });
184
-
185
- let expires = await forge.readCertificateInfo(cert);
186
- expires = expires.notAfter;
187
- this.setCertificate(
188
- host,
189
- expires,
190
- organization_id,
191
- hostKeyPath,
192
- cert,
193
- key
194
- );
195
-
196
- key = key.toString("utf8");
197
-
198
- this.crud.send({
199
- method: "object.update",
200
- host,
201
- array: "organizations",
202
- object: {
203
- _id: organization_id,
204
- [`host[${hostPosition}]`]: {
205
- ...hostObject,
206
- name: host,
207
- cert,
208
- key,
209
- expires
210
- }
211
- },
212
- organization_id
213
- });
214
-
215
- console.log(`Certificate successfully created for ${host}!'`);
216
- return true;
217
- } catch (error) {
218
- console.log(`Certificate failed to create for ${host}!`);
219
- delete hosts[host];
220
- return false;
221
- }
222
- }
223
-
224
- async getCertificate(host, organization_id) {
225
- const hostKeyPath = keyPath + host + "/";
226
- let hostPosition, hostObject;
227
-
228
- let organization = await this.crud.getOrganization({
229
- host,
230
- organization_id
231
- });
232
- if (organization.error) return false;
233
- if (!organization_id) organization_id = organization._id;
234
-
235
- if (organization.host) {
236
- for (let i = 0; i < organization.host.length; i++) {
237
- if (organization.host[i].name === host) {
238
- hostPosition = i;
239
- hostObject = organization.host[i];
240
- if (organization.host[i].cert && organization.host[i].key) {
241
- let expires = await forge.readCertificateInfo(
242
- organization.host[i].cert
243
- );
244
- expires = expires.notAfter;
245
- if (this.isValid(expires)) {
246
- this.setCertificate(
247
- host,
248
- expires,
249
- organization_id,
250
- hostKeyPath,
251
- organization.host[i].cert,
252
- organization.host[i].key
253
- );
254
- return true;
255
- }
256
- }
257
- break;
258
- }
259
- }
260
-
261
- if (!hostPosition && hostPosition !== 0) return false;
262
- }
263
-
264
- return await this.requestCertificate(
265
- host,
266
- hostPosition,
267
- hostObject,
268
- organization_id,
269
- false
270
- );
271
- }
272
-
273
- async checkCertificate(host, organization_id, pathname = "") {
274
- let hostname = host.split(":")[0];
275
- if (
276
- hostname === "localhost" ||
277
- hostname === "127.0.0.1" ||
278
- pathname.startsWith("/.well-known/acme-challenge/")
279
- )
280
- return true;
281
-
282
- if (certificates[host]) {
283
- return true;
284
- }
285
-
286
- const hostKeyPath = keyPath + host + "/";
287
- if (fs.existsSync(hostKeyPath + "fullchain.pem")) {
288
- let cert = fs.readFileSync(hostKeyPath + "fullchain.pem", "utf8");
289
- let expires = await forge.readCertificateInfo(cert);
290
- expires = expires.notAfter;
291
- if (this.isValid(expires)) {
292
- this.setCertificate(host, expires, organization_id);
293
- return true;
294
- }
295
- }
296
-
297
- if (!hosts[host])
298
- hosts[host] = await this.getCertificate(host, organization_id);
299
- return hosts[host];
300
- }
301
-
302
- isValid(expires) {
303
- let currentDate = new Date();
304
- currentDate.setDate(currentDate.getDate() + DAYS);
305
- currentDate.setHours(currentDate.getHours() + HOURS);
306
- currentDate.setMinutes(currentDate.getMinutes() + MINUTES);
307
-
308
- if (expires && currentDate < expires) {
309
- return true; // SSL is still valid, no need to renew
310
- }
311
- }
312
-
313
- setCertificate(host, expires, organization_id, hostKeyPath, cert, key) {
314
- // let expireDate = new Date(expires);
315
- // let currentDate = new Date();
316
-
317
- // Adjust the expireDate by the DAYS, HOURS, and MINUTES constants
318
- // expireDate.setDate(expireDate.getDate() - DAYS); // Subtracting to renew earlier
319
- // expireDate.setHours(expireDate.getHours() - HOURS);
320
- // expireDate.setMinutes(expireDate.getMinutes() - MINUTES);
321
-
322
- // Calculate the time difference in milliseconds
323
- // let timeoutDuration = expireDate.getTime() - currentDate.getTime();
324
-
325
- // Ensure we're not setting a negative timeout in case of past dates or errors
326
- // timeoutDuration = Math.max(timeoutDuration, 0);
327
-
328
- // Clear any existing timeout for the host
329
- // if (certificates[host] && certificates[host].timeout) {
330
- // clearTimeout(certificates[host].timeout);
331
- // }
332
-
333
- // Set the timeout to call checkCertificate before the actual expiration
334
- // let timeout = setTimeout(() => {
335
- // this.checkCertificate(host, organization_id);
336
- // }, timeoutDuration);
337
-
338
- // Store the timeout and organization_id for later reference or cancellation
339
-
340
- if (hostKeyPath) {
341
- if (!fs.existsSync(hostKeyPath)) {
342
- fs.mkdirSync(hostKeyPath, { recursive: true });
343
- }
344
- fs.writeFileSync(hostKeyPath + "fullchain.pem", cert);
345
- // fs.chmodSync(keyPath + 'fullchain.pem', '444')
346
- fs.writeFileSync(hostKeyPath + "private-key.pem", key);
347
- // fs.chmodSync(keyPath + 'private-key.pem', '400')
348
- }
349
-
350
- this.proxy.createServer(host);
351
-
352
- certificates[host] = { expires, organization_id };
353
- }
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;
55
+
56
+ if (!fs.existsSync(accountKeyPath)) {
57
+ accountKey = await forge.createPrivateKey();
58
+ isNewAccount = true;
59
+ fs.writeFileSync(accountKeyPath, accountKey);
60
+ } else {
61
+ accountKey = fs.readFileSync(accountKeyPath, "utf8");
62
+ }
63
+
64
+ client = new Client({
65
+ directoryUrl: "https://acme-v02.api.letsencrypt.org/directory",
66
+ accountKey: accountKey
67
+ });
68
+
69
+ if (isNewAccount) {
70
+ try {
71
+ await client.createAccount({
72
+ termsOfServiceAgreed: true,
73
+ contact: [`mailto:${email}`]
74
+ });
75
+ console.log("[@cocreate/acme] ACME authority account created successfully!");
76
+ } catch (error) {
77
+ try {
78
+ fs.unlinkSync(accountKeyPath);
79
+ } catch (_) {}
80
+ console.error("[@cocreate/acme] Error creating account credentials:", error.message);
81
+ }
82
+ }
83
+
84
+
85
+ } catch (err) {
86
+ console.error("[@cocreate/acme] Error initializing certificate client:", err);
87
+ throw err;
88
+ }
89
+ }
90
+
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
+ }
108
+
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
+ }
131
+
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
+ }
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;
171
+ }
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
+ });
185
+
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");
191
+
192
+ if (server && server.crud) {
193
+ server.crud.send({
194
+ method: "object.update",
195
+ host,
196
+ array: "organizations",
197
+ object: {
198
+ _id: organization_id,
199
+ [`host[${hostPosition}]`]: { ...hostObject, name: host, cert, key, expires }
200
+ },
201
+ organization_id
202
+ });
203
+ }
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;
210
+ }
211
+ }
212
+
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;
228
+ }
229
+
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
+ }
235
+
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;
252
+ }
253
+ }
254
+ if (!hostPosition && hostPosition !== 0) return false;
255
+ }
256
+
257
+ return await request(host, hostPosition, hostObject, organization_id, false);
258
+ }
259
+
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;
279
+ }
280
+ }
281
+
282
+ if (!hosts[host]) hosts[host] = await get(host, organization_id);
283
+ return hosts[host];
284
+ }
285
+
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);
294
+
295
+ return (expires && currentDate < expires);
296
+ }
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
+ }
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 };
354
313
  }
355
314
 
356
- 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
+ };