@cocreate/acme 1.4.1 → 1.5.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.
- package/.github/workflows/automated.yml +1 -1
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/release.config.js +1 -1
- package/src/index.js +76 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.5.1](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.5.0...v1.5.1) (2026-07-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* set default branch to main ([383f061](https://github.com/CoCreate-app/CoCreate-acme/commit/383f061a286d5f0016c33c28acca72c79af3edf9))
|
|
7
|
+
|
|
8
|
+
# [1.5.0](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.4.1...v1.5.0) (2026-07-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* enhance DNS verification and secure file handling in ACME client ([0c92815](https://github.com/CoCreate-app/CoCreate-acme/commit/0c92815879b5a390b1b4697a8d530294bb78b1dd))
|
|
14
|
+
|
|
1
15
|
## [1.4.1](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.4.0...v1.4.1) (2026-07-17)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/acme",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
package/release.config.js
CHANGED
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
|
|
22
|
-
import
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
305
|
-
fs.writeFileSync(`${hostKeyPath}
|
|
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
|