@cocreate/acme 1.4.1 → 1.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.5.0](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.4.1...v1.5.0) (2026-07-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * enhance DNS verification and secure file handling in ACME client ([0c92815](https://github.com/CoCreate-app/CoCreate-acme/commit/0c92815879b5a390b1b4697a8d530294bb78b1dd))
7
+
1
8
  ## [1.4.1](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.4.0...v1.4.1) (2026-07-17)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/acme",
3
- "version": "1.4.1",
3
+ "version": "1.5.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",
package/src/index.js CHANGED
@@ -18,25 +18,60 @@
18
18
  import { Client, forge } from "acme-client";
19
19
  import fs from "fs";
20
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);
21
+ import path from "path";
22
+ import dns from "node:dns/promises";
25
23
 
26
24
  // ==========================================
27
25
  // Module-Level Sandbox State (ESM Singleton Container)
28
26
  // ==========================================
29
27
  let server = null; // Direct reference used across all module functions
30
- const certificates = {};
31
- const email = "ssl@cocreate.app";
32
- const keyPath = "/etc/certificates/";
28
+ const email = "ssl@cocreatejs.com";
29
+
30
+ // Local storage: No sudo required, keeps files isolated to the application runtime
31
+ const keyPath = path.join(process.cwd(), "certificates") + path.sep;
32
+
33
33
  let client;
34
34
  const hosts = {};
35
+ const certificates = {};
36
+
37
+ // ==========================================
38
+ // SNI Memory Management
39
+ // ==========================================
40
+ const secureContextCache = new Map();
41
+ const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
42
+
43
+ // Background Garbage Collection for inactive contexts
44
+ setInterval(() => {
45
+ const now = Date.now();
46
+ for (const [domain, data] of secureContextCache.entries()) {
47
+ if (now - data.timestamp > CACHE_TTL) {
48
+ secureContextCache.delete(domain);
49
+ }
50
+ }
51
+ }, 60 * 1000).unref(); // Unref prevents this timer from blocking a clean process exit
35
52
 
36
53
  const DAYS = Math.floor(Math.random() * 7);
37
54
  const HOURS = Math.floor(Math.random() * 24);
38
55
  const MINUTES = Math.floor(Math.random() * 60);
39
56
 
57
+ /**
58
+ * Executes a DNS lookup to ensure the domain actually points to this infrastructure
59
+ * before requesting a certificate, preventing ACME rate limit bans.
60
+ */
61
+ async function verifyDnsBeforeAcme(domain) {
62
+ try {
63
+ const addresses = await dns.resolve4(domain);
64
+ // If the server knows its own IP, strictly match it. Otherwise, ensure it resolves to *something*.
65
+ if (server && server.ip && addresses.length > 0) {
66
+ return addresses.includes(server.ip);
67
+ }
68
+ return addresses.length > 0;
69
+ } catch (err) {
70
+ console.warn(`[@cocreate/acme] DNS pre-flight failed for ${domain}. Skipping Let's Encrypt request.`);
71
+ return false;
72
+ }
73
+ }
74
+
40
75
  /**
41
76
  * Bootstraps the ACME account keys, directories, and instantiates the
42
77
  * secure HTTPS server context, attaching it back to the parent server reference.
@@ -46,8 +81,10 @@ export async function init(parentServer) {
46
81
  try {
47
82
  server = parentServer;
48
83
 
49
- await exec("sudo mkdir -p /etc/certificates");
50
- await exec("sudo chmod 777 /etc/certificates");
84
+ // Native folder creation with secure, owner-only read/write/execute permissions
85
+ if (!fs.existsSync(keyPath)) {
86
+ fs.mkdirSync(keyPath, { recursive: true, mode: 0o700 });
87
+ }
51
88
 
52
89
  const accountKeyPath = `${keyPath}account.pem`;
53
90
  let accountKey = "";
@@ -56,7 +93,7 @@ export async function init(parentServer) {
56
93
  if (!fs.existsSync(accountKeyPath)) {
57
94
  accountKey = await forge.createPrivateKey();
58
95
  isNewAccount = true;
59
- fs.writeFileSync(accountKeyPath, accountKey);
96
+ fs.writeFileSync(accountKeyPath, accountKey, { mode: 0o600 });
60
97
  } else {
61
98
  accountKey = fs.readFileSync(accountKeyPath, "utf8");
62
99
  }
@@ -80,8 +117,6 @@ export async function init(parentServer) {
80
117
  console.error("[@cocreate/acme] Error creating account credentials:", error.message);
81
118
  }
82
119
  }
83
-
84
-
85
120
  } catch (err) {
86
121
  console.error("[@cocreate/acme] Error initializing certificate client:", err);
87
122
  throw err;
@@ -107,18 +142,32 @@ export function load(domain) {
107
142
  }
108
143
 
109
144
  /**
110
- * Core SNI Handshake implementation. Resolves domains dynamically and loads TLS Context.
145
+ * Core SNI Handshake implementation. Resolves domains dynamically and caches TLS Contexts in RAM.
111
146
  * @param {string} domain - Incoming target SNI hostname
112
147
  * @param {Function} cb - Resolution completion callback
113
148
  */
114
149
  export async function sniCallback(domain, cb) {
115
150
  try {
116
- // Verify domain authorization
151
+ // 1. Check RAM Cache First (Sub-millisecond response)
152
+ if (secureContextCache.has(domain)) {
153
+ const cacheEntry = secureContextCache.get(domain);
154
+ cacheEntry.timestamp = Date.now(); // Renew TTL
155
+ return cb(null, cacheEntry.context);
156
+ }
157
+
158
+ // 2. Fallback to Disk / Database / ACME creation
117
159
  const isValidDomain = await check(domain);
118
160
 
119
161
  if (isValidDomain) {
120
162
  const credentials = load(domain);
121
163
  const sslContext = tls.createSecureContext(credentials);
164
+
165
+ // 3. Store Compiled C++ Context in RAM
166
+ secureContextCache.set(domain, {
167
+ context: sslContext,
168
+ timestamp: Date.now()
169
+ });
170
+
122
171
  cb(null, sslContext);
123
172
  } else {
124
173
  cb(new Error(`[@cocreate/acme] Unauthorized secure request for domain: ${domain}`));
@@ -137,7 +186,7 @@ export async function request(host, hostPosition, hostObject, organization_id, w
137
186
  const hostKeyPath = `${keyPath}${host}/`;
138
187
 
139
188
  if (!fs.existsSync(hostKeyPath)) {
140
- fs.mkdirSync(hostKeyPath, { recursive: true });
189
+ fs.mkdirSync(hostKeyPath, { recursive: true, mode: 0o700 });
141
190
  }
142
191
 
143
192
  const domains = wildcard ? [host, `*.${host}`] : [host];
@@ -254,6 +303,13 @@ export async function get(host, organization_id) {
254
303
  if (!hostPosition && hostPosition !== 0) return false;
255
304
  }
256
305
 
306
+ // --- PRE-FLIGHT DNS CHECK ---
307
+ // Prevent contacting ACME if the domain's DNS is not yet pointing to this node
308
+ const dnsValid = await verifyDnsBeforeAcme(host);
309
+ if (!dnsValid) {
310
+ return false;
311
+ }
312
+
257
313
  return await request(host, hostPosition, hostObject, organization_id, false);
258
314
  }
259
315
 
@@ -296,13 +352,14 @@ export function isValid(expires) {
296
352
  }
297
353
 
298
354
  /**
299
- * Writes dynamic credentials to disk and triggers an update on the host ingress config if applicable.
355
+ * Writes dynamic credentials to disk securely and updates local tracking state.
300
356
  */
301
357
  export function set(host, expires, organization_id, hostKeyPath, cert, key) {
302
358
  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);
359
+ if (!fs.existsSync(hostKeyPath)) fs.mkdirSync(hostKeyPath, { recursive: true, mode: 0o700 });
360
+ // Enforce owner-only read/write permissions for sensitive key data
361
+ fs.writeFileSync(`${hostKeyPath}fullchain.pem`, cert, { mode: 0o600 });
362
+ fs.writeFileSync(`${hostKeyPath}private-key.pem`, key, { mode: 0o600 });
306
363
  }
307
364
 
308
365
  // Safely bypassed if Nginx is entirely removed/omitted from this deployment