certops 1.0.15 → 1.0.16
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/dist/api.js +8 -2
- package/dist/commands/download.js +9 -1
- package/dist/commands/service/configure.js +8 -7
- package/dist/commands/service/control.js +2 -1
- package/dist/commands/service/daemon.js +2 -3
- package/dist/commands/service/install.js +1 -5
- package/dist/config.js +2 -0
- package/dist/files.js +3 -2
- package/dist/hooks.js +7 -1
- package/dist/logger.js +41 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
2
|
+
function fetchWithTimeout(url, init) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
5
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
6
|
+
}
|
|
1
7
|
export function createApiClient(options) {
|
|
2
8
|
const base = (options.apiUrl ?? 'https://ssl-manager.dcom.at').replace(/\/$/, '');
|
|
3
9
|
const baseHeaders = {
|
|
@@ -20,7 +26,7 @@ export function createApiClient(options) {
|
|
|
20
26
|
throw new Error(msg);
|
|
21
27
|
}
|
|
22
28
|
async function request(path) {
|
|
23
|
-
const res = await
|
|
29
|
+
const res = await fetchWithTimeout(`${base}/api/cli${path}`, {
|
|
24
30
|
headers: { ...baseHeaders, 'Content-Type': 'application/json' },
|
|
25
31
|
});
|
|
26
32
|
const body = (await res.json());
|
|
@@ -31,7 +37,7 @@ export function createApiClient(options) {
|
|
|
31
37
|
return body.data;
|
|
32
38
|
}
|
|
33
39
|
async function requestBinary(path) {
|
|
34
|
-
const res = await
|
|
40
|
+
const res = await fetchWithTimeout(`${base}/api/cli${path}`, {
|
|
35
41
|
headers: baseHeaders,
|
|
36
42
|
});
|
|
37
43
|
if (!res.ok) {
|
|
@@ -3,10 +3,14 @@ import select from '@inquirer/select';
|
|
|
3
3
|
import {} from '../api.js';
|
|
4
4
|
import { getConfiguredClient } from '../client.js';
|
|
5
5
|
import { saveZip } from '../files.js';
|
|
6
|
+
import { updateCertState } from '../state.js';
|
|
7
|
+
import { createLogger, logError } from '../logger.js';
|
|
8
|
+
const log = createLogger('download');
|
|
6
9
|
async function performDownload(cert, client) {
|
|
7
10
|
process.stdout.write(`\nDownloading ${cert.certName}…\n`);
|
|
8
11
|
const zipBuffer = await client.downloadCert(cert._id);
|
|
9
12
|
const paths = await saveZip(cert.certName, zipBuffer);
|
|
13
|
+
await updateCertState(cert.certName, cert.expiryDate ?? '');
|
|
10
14
|
process.stdout.write(` ✓ Fullchain : ${paths.fullchainPath}\n`);
|
|
11
15
|
process.stdout.write(` ✓ Certificate : ${paths.certPath}\n`);
|
|
12
16
|
process.stdout.write(` ✓ Chain : ${paths.chainPath}\n`);
|
|
@@ -15,6 +19,8 @@ async function performDownload(cert, client) {
|
|
|
15
19
|
process.stdout.write(` ✓ Expires : ${new Date(cert.expiryDate).toLocaleDateString()}\n`);
|
|
16
20
|
}
|
|
17
21
|
process.stdout.write('\n');
|
|
22
|
+
const expiry = cert.expiryDate ? ` expires ${new Date(cert.expiryDate).toLocaleDateString()}` : '';
|
|
23
|
+
log(`Downloaded ${cert.certName}${expiry} → ${paths.fullchainPath}`);
|
|
18
24
|
}
|
|
19
25
|
export const downloadCommand = new Command('download')
|
|
20
26
|
.description('Download a certificate to /etc/certops/certs/<domain>/ — requires sudo')
|
|
@@ -81,7 +87,9 @@ Examples:
|
|
|
81
87
|
catch (err) {
|
|
82
88
|
if (err.name === 'ExitPromptError')
|
|
83
89
|
process.exit(0);
|
|
84
|
-
|
|
90
|
+
const msg = err.message;
|
|
91
|
+
process.stderr.write(`\nError: ${msg}\n\n`);
|
|
92
|
+
logError(msg);
|
|
85
93
|
process.exit(1);
|
|
86
94
|
}
|
|
87
95
|
});
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import input from '@inquirer/input';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
|
-
import { readConfig, writeConfig } from '../../config.js';
|
|
4
|
+
import { readConfig, writeConfig, CERTOPS_DIR } from '../../config.js';
|
|
5
|
+
import { BRAND } from '../../brand.js';
|
|
5
6
|
export const configureCommand = new Command('configure')
|
|
6
7
|
.description('Update service config (watch domains, check interval) and restart')
|
|
7
8
|
.action(async () => {
|
|
8
9
|
try {
|
|
9
10
|
if (process.getuid?.() !== 0) {
|
|
10
|
-
console.error(
|
|
11
|
-
console.error(
|
|
11
|
+
console.error(`Error: ${BRAND.binName} service configure must run as root.`);
|
|
12
|
+
console.error(`\n sudo ${BRAND.binName} service configure\n`);
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
const existing = await readConfig();
|
|
15
|
-
console.log(
|
|
16
|
+
console.log(`\n${BRAND.displayName} — Update Service Config\n`);
|
|
16
17
|
console.log(' (API key is stored in the systemd unit, not changed here)\n');
|
|
17
18
|
const intervalStr = await input({
|
|
18
19
|
message: 'Check interval (hours):',
|
|
@@ -44,8 +45,8 @@ export const configureCommand = new Command('configure')
|
|
|
44
45
|
maxDownloadRetries: Number(retriesStr),
|
|
45
46
|
watchDomains,
|
|
46
47
|
});
|
|
47
|
-
console.log(
|
|
48
|
-
const result = spawnSync('systemctl', ['restart',
|
|
48
|
+
console.log(`\n✓ Config updated at ${CERTOPS_DIR}/config.json`);
|
|
49
|
+
const result = spawnSync('systemctl', ['restart', BRAND.serviceUnit], {
|
|
49
50
|
stdio: ['inherit', 'inherit', 'pipe'],
|
|
50
51
|
encoding: 'utf8',
|
|
51
52
|
});
|
|
@@ -53,7 +54,7 @@ export const configureCommand = new Command('configure')
|
|
|
53
54
|
if (result.stderr?.trim())
|
|
54
55
|
process.stderr.write(result.stderr + '\n');
|
|
55
56
|
process.stderr.write('\nWarning: config saved but service restart failed.\n');
|
|
56
|
-
process.stderr.write(
|
|
57
|
+
process.stderr.write(` Run: sudo ${BRAND.binName} service start\n\n`);
|
|
57
58
|
process.exit(1);
|
|
58
59
|
}
|
|
59
60
|
console.log('✓ Service restarted with new config.\n');
|
|
@@ -4,6 +4,7 @@ import { unlink } from 'fs/promises';
|
|
|
4
4
|
import { resolveApiKey, runRenewalCycle } from './renew.js';
|
|
5
5
|
import { BRAND } from '../../brand.js';
|
|
6
6
|
import { CERTOPS_DIR } from '../../config.js';
|
|
7
|
+
import { createLogger } from '../../logger.js';
|
|
7
8
|
const UNIT = BRAND.serviceUnit;
|
|
8
9
|
const UNIT_PATH = `/etc/systemd/system/${BRAND.serviceUnit}.service`;
|
|
9
10
|
function systemctl(args, exitOnFail = true) {
|
|
@@ -49,7 +50,7 @@ export const checkCommand = new Command('check')
|
|
|
49
50
|
process.stderr.write(` Set the env var or run: sudo ${BRAND.binName} service install\n\n`);
|
|
50
51
|
process.exit(1);
|
|
51
52
|
}
|
|
52
|
-
const log = (
|
|
53
|
+
const log = createLogger();
|
|
53
54
|
process.stdout.write('\nRunning renewal check…\n\n');
|
|
54
55
|
try {
|
|
55
56
|
const result = await runRenewalCycle(apiKey, log);
|
|
@@ -2,9 +2,8 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { readConfig } from '../../config.js';
|
|
3
3
|
import { resolveApiKey, runRenewalCycle } from './renew.js';
|
|
4
4
|
import { BRAND } from '../../brand.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
import { createLogger } from '../../logger.js';
|
|
6
|
+
const log = createLogger();
|
|
8
7
|
function sleep(ms) {
|
|
9
8
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
9
|
}
|
|
@@ -57,10 +57,6 @@ export const installCommand = new Command('install')
|
|
|
57
57
|
? true
|
|
58
58
|
: `Key must start with ${BRAND.keyPrefix}`,
|
|
59
59
|
});
|
|
60
|
-
const apiUrl = await input({
|
|
61
|
-
message: `API URL (leave blank for default ${BRAND.apiUrl}):`,
|
|
62
|
-
default: existing.apiUrl ?? '',
|
|
63
|
-
});
|
|
64
60
|
const intervalStr = await input({
|
|
65
61
|
message: 'Check interval (hours):',
|
|
66
62
|
default: String(existing.checkIntervalHours),
|
|
@@ -75,7 +71,7 @@ export const installCommand = new Command('install')
|
|
|
75
71
|
.map(d => d.trim())
|
|
76
72
|
.filter(Boolean);
|
|
77
73
|
const config = {
|
|
78
|
-
apiUrl: apiUrl
|
|
74
|
+
apiUrl: existing.apiUrl,
|
|
79
75
|
renewalThresholdDays: existing.renewalThresholdDays,
|
|
80
76
|
checkIntervalHours: Number(intervalStr),
|
|
81
77
|
watchDomains,
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,8 @@ import { join } from 'path';
|
|
|
3
3
|
import { BRAND } from './brand.js';
|
|
4
4
|
export const CERTOPS_DIR = BRAND.configDir;
|
|
5
5
|
export const HOOKS_DIR = join(CERTOPS_DIR, 'hooks');
|
|
6
|
+
export const LOG_DIR = `/var/log/${BRAND.name}`;
|
|
7
|
+
export const LOG_FILE = join(LOG_DIR, `${BRAND.name}.log`);
|
|
6
8
|
const CONFIG_PATH = join(CERTOPS_DIR, 'config.json');
|
|
7
9
|
const DEFAULTS = {
|
|
8
10
|
renewalThresholdDays: 5,
|
package/dist/files.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, writeFile, chmod } from 'fs/promises';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { unzipSync } from 'fflate';
|
|
4
4
|
import { CERTOPS_DIR } from './config.js';
|
|
5
|
+
import { BRAND } from './brand.js';
|
|
5
6
|
export async function saveZip(certName, zipBuffer) {
|
|
6
7
|
const zipDirName = certName.replace(/^\*\./, 'wildcard.');
|
|
7
8
|
const dir = join(CERTOPS_DIR, 'certs', zipDirName);
|
|
@@ -45,8 +46,8 @@ function fsError(err, certName, path) {
|
|
|
45
46
|
const e = err instanceof Error ? err : null;
|
|
46
47
|
if (e?.code === 'EACCES' || e?.code === 'EPERM') {
|
|
47
48
|
process.stderr.write(`\nPermission denied: ${path}\n`);
|
|
48
|
-
process.stderr.write(
|
|
49
|
-
process.stderr.write(` sudo
|
|
49
|
+
process.stderr.write(`${CERTOPS_DIR} requires root. Re-run:\n\n`);
|
|
50
|
+
process.stderr.write(` sudo ${BRAND.binName} download '${certName}'\n\n`);
|
|
50
51
|
process.exit(1);
|
|
51
52
|
}
|
|
52
53
|
throw err;
|
package/dist/hooks.js
CHANGED
|
@@ -17,19 +17,25 @@ async function fileExists(path) {
|
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
const HOOK_TIMEOUT_MS = 60_000;
|
|
20
21
|
async function execHook(scriptPath, vars) {
|
|
21
22
|
return new Promise((resolve, reject) => {
|
|
22
23
|
const child = spawn('bash', [scriptPath], {
|
|
23
24
|
env: { ...process.env, ...vars },
|
|
24
25
|
stdio: 'inherit',
|
|
25
26
|
});
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
child.kill('SIGTERM');
|
|
29
|
+
reject(new Error(`Hook timed out after ${HOOK_TIMEOUT_MS / 1000}s`));
|
|
30
|
+
}, HOOK_TIMEOUT_MS);
|
|
26
31
|
child.on('close', (code) => {
|
|
32
|
+
clearTimeout(timer);
|
|
27
33
|
if (code === 0)
|
|
28
34
|
resolve();
|
|
29
35
|
else
|
|
30
36
|
reject(new Error(`Hook exited with code ${code}`));
|
|
31
37
|
});
|
|
32
|
-
child.on('error', reject);
|
|
38
|
+
child.on('error', (err) => { clearTimeout(timer); reject(err); });
|
|
33
39
|
});
|
|
34
40
|
}
|
|
35
41
|
export async function runHooks(certName, vars, log) {
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { LOG_DIR, LOG_FILE } from './config.js';
|
|
3
|
+
let logDirReady = false;
|
|
4
|
+
async function ensureLogDir() {
|
|
5
|
+
if (logDirReady)
|
|
6
|
+
return;
|
|
7
|
+
try {
|
|
8
|
+
await mkdir(LOG_DIR, { recursive: true });
|
|
9
|
+
logDirReady = true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// silently fail — never crash the daemon over a logging error
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function writeToFile(line) {
|
|
16
|
+
await ensureLogDir();
|
|
17
|
+
if (!logDirReady)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
await appendFile(LOG_FILE, line + '\n', { encoding: 'utf8', mode: 0o640 });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// silently fail
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function timestamp() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
export function createLogger(tag) {
|
|
30
|
+
const prefix = tag ? `[${tag}] ` : '';
|
|
31
|
+
return (msg) => {
|
|
32
|
+
const line = `[${timestamp()}] ${prefix}${msg}`;
|
|
33
|
+
process.stdout.write(line + '\n');
|
|
34
|
+
void writeToFile(line);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function logError(msg) {
|
|
38
|
+
const line = `[${timestamp()}] [ERROR] ${msg}`;
|
|
39
|
+
process.stderr.write(line + '\n');
|
|
40
|
+
void writeToFile(line);
|
|
41
|
+
}
|