certops 1.0.11 → 1.0.13
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/README.md +2 -2
- package/dist/brand.js +10 -0
- package/dist/client.js +9 -7
- package/dist/commands/service/control.js +15 -17
- package/dist/commands/service/daemon.js +3 -2
- package/dist/commands/service/install.js +28 -30
- package/dist/commands/service/renew.js +11 -9
- package/dist/config.js +2 -1
- package/dist/hooks.js +2 -1
- package/dist/index.js +10 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ npm install -g certops
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
13
|
- Node.js 18+
|
|
14
|
-
-
|
|
14
|
+
- A CertOps API key (`co_...`) from your dashboard
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
# Set your API key
|
|
20
|
-
export CERTOPS_API_KEY='
|
|
20
|
+
export CERTOPS_API_KEY='co_...'
|
|
21
21
|
|
|
22
22
|
# List certificates
|
|
23
23
|
certops list
|
package/dist/brand.js
ADDED
package/dist/client.js
CHANGED
|
@@ -2,21 +2,23 @@ import input from '@inquirer/input';
|
|
|
2
2
|
import { readConfig } from './config.js';
|
|
3
3
|
import { createApiClient } from './api.js';
|
|
4
4
|
import { CLI_VERSION } from './version.js';
|
|
5
|
+
import { BRAND } from './brand.js';
|
|
6
|
+
const API_KEY_ENV = `${BRAND.envPrefix}_API_KEY`;
|
|
5
7
|
export async function getConfiguredClient() {
|
|
6
|
-
let apiKey = process.env[
|
|
8
|
+
let apiKey = process.env[API_KEY_ENV];
|
|
7
9
|
if (!apiKey) {
|
|
8
10
|
if (!process.stdin.isTTY) {
|
|
9
|
-
process.stderr.write(
|
|
10
|
-
process.stderr.write(
|
|
11
|
+
process.stderr.write(`Error: ${API_KEY_ENV} is not set and no TTY is available for prompt.\n`);
|
|
12
|
+
process.stderr.write(` export ${API_KEY_ENV}='${BRAND.keyPrefix}...'\n\n`);
|
|
11
13
|
process.exit(1);
|
|
12
14
|
}
|
|
13
15
|
apiKey = await input({
|
|
14
|
-
message:
|
|
15
|
-
validate: (v) => v.startsWith(
|
|
16
|
+
message: `API key (${BRAND.keyPrefix}...):`,
|
|
17
|
+
validate: (v) => v.startsWith(BRAND.keyPrefix) && v.length > 10
|
|
16
18
|
? true
|
|
17
|
-
:
|
|
19
|
+
: `Key must start with ${BRAND.keyPrefix}`,
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
const config = await readConfig();
|
|
21
|
-
return createApiClient({ apiKey, apiUrl: config.apiUrl, cliVersion: CLI_VERSION });
|
|
23
|
+
return createApiClient({ apiKey, apiUrl: config.apiUrl ?? BRAND.apiUrl, cliVersion: CLI_VERSION });
|
|
22
24
|
}
|
|
@@ -2,13 +2,10 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
3
|
import { unlink } from 'fs/promises';
|
|
4
4
|
import { resolveApiKey, runRenewalCycle } from './renew.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Stderr is only printed if the command fails, so noisy shim warnings
|
|
10
|
-
* from Python-based systemctl stubs don't pollute normal output.
|
|
11
|
-
*/
|
|
5
|
+
import { BRAND } from '../../brand.js';
|
|
6
|
+
import { CERTOPS_DIR } from '../../config.js';
|
|
7
|
+
const UNIT = BRAND.serviceUnit;
|
|
8
|
+
const UNIT_PATH = `/etc/systemd/system/${BRAND.serviceUnit}.service`;
|
|
12
9
|
function systemctl(args, exitOnFail = true) {
|
|
13
10
|
const result = spawnSync('systemctl', args, {
|
|
14
11
|
stdio: ['inherit', 'inherit', 'pipe'],
|
|
@@ -30,25 +27,26 @@ function requireRoot() {
|
|
|
30
27
|
}
|
|
31
28
|
}
|
|
32
29
|
export const startCommand = new Command('start')
|
|
33
|
-
.description(
|
|
30
|
+
.description(`Start the ${BRAND.displayName} service`)
|
|
34
31
|
.action(() => { requireRoot(); systemctl(['start', UNIT]); });
|
|
35
32
|
export const stopCommand = new Command('stop')
|
|
36
|
-
.description(
|
|
33
|
+
.description(`Stop the ${BRAND.displayName} service`)
|
|
37
34
|
.action(() => { requireRoot(); systemctl(['stop', UNIT]); });
|
|
38
35
|
export const restartCommand = new Command('restart')
|
|
39
36
|
.description('Restart the service — picks up config.json changes immediately')
|
|
40
37
|
.action(() => { requireRoot(); systemctl(['restart', UNIT]); });
|
|
41
38
|
export const statusCommand = new Command('status')
|
|
42
|
-
.description(
|
|
39
|
+
.description(`Show ${BRAND.displayName} service status`)
|
|
43
40
|
.action(() => { systemctl(['status', UNIT]); });
|
|
44
41
|
export const checkCommand = new Command('check')
|
|
45
42
|
.description('Run one renewal check cycle immediately (for testing)')
|
|
46
43
|
.action(async () => {
|
|
47
44
|
requireRoot();
|
|
48
45
|
const apiKey = await resolveApiKey();
|
|
46
|
+
const apiKeyEnv = `${BRAND.envPrefix}_API_KEY`;
|
|
49
47
|
if (!apiKey) {
|
|
50
|
-
process.stderr.write(
|
|
51
|
-
process.stderr.write(
|
|
48
|
+
process.stderr.write(`\nError: ${apiKeyEnv} not found in environment or service unit.\n`);
|
|
49
|
+
process.stderr.write(` Set the env var or run: sudo ${BRAND.binName} service install\n\n`);
|
|
52
50
|
process.exit(1);
|
|
53
51
|
}
|
|
54
52
|
const log = (msg) => process.stdout.write(` ${msg}\n`);
|
|
@@ -65,10 +63,10 @@ export const checkCommand = new Command('check')
|
|
|
65
63
|
}
|
|
66
64
|
});
|
|
67
65
|
export const uninstallServiceCommand = new Command('uninstall')
|
|
68
|
-
.description(
|
|
66
|
+
.description(`Stop, disable, and remove the ${BRAND.displayName} service unit`)
|
|
69
67
|
.action(async () => {
|
|
70
68
|
requireRoot();
|
|
71
|
-
process.stdout.write(
|
|
69
|
+
process.stdout.write(`\nRemoving ${BRAND.displayName} service…\n\n`);
|
|
72
70
|
systemctl(['stop', UNIT], false);
|
|
73
71
|
systemctl(['disable', UNIT], false);
|
|
74
72
|
try {
|
|
@@ -80,7 +78,7 @@ export const uninstallServiceCommand = new Command('uninstall')
|
|
|
80
78
|
throw err;
|
|
81
79
|
}
|
|
82
80
|
systemctl(['daemon-reload'], false);
|
|
83
|
-
process.stdout.write(
|
|
84
|
-
process.stdout.write(
|
|
85
|
-
process.stdout.write(
|
|
81
|
+
process.stdout.write(`\n ✓ Service uninstalled.\n`);
|
|
82
|
+
process.stdout.write(` ${CERTOPS_DIR}/ (config, state, certs, hooks) was NOT removed.\n`);
|
|
83
|
+
process.stdout.write(` Run "sudo ${BRAND.binName} service install" to reinstall.\n\n`);
|
|
86
84
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { readConfig } from '../../config.js';
|
|
3
3
|
import { resolveApiKey, runRenewalCycle } from './renew.js';
|
|
4
|
+
import { BRAND } from '../../brand.js';
|
|
4
5
|
function log(msg) {
|
|
5
6
|
process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
|
|
6
7
|
}
|
|
@@ -10,7 +11,7 @@ function sleep(ms) {
|
|
|
10
11
|
async function tick() {
|
|
11
12
|
const apiKey = await resolveApiKey();
|
|
12
13
|
if (!apiKey) {
|
|
13
|
-
log(
|
|
14
|
+
log(`${BRAND.envPrefix}_API_KEY not available — skipping cycle.`);
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
17
|
try {
|
|
@@ -24,7 +25,7 @@ async function tick() {
|
|
|
24
25
|
export const daemonCommand = new Command('run')
|
|
25
26
|
.description('Run the monitoring daemon (managed by systemd)')
|
|
26
27
|
.action(async () => {
|
|
27
|
-
log(
|
|
28
|
+
log(`${BRAND.displayName} daemon starting.`);
|
|
28
29
|
process.on('SIGTERM', () => { log('SIGTERM — shutting down.'); process.exit(0); });
|
|
29
30
|
process.on('SIGINT', () => { log('SIGINT — shutting down.'); process.exit(0); });
|
|
30
31
|
while (true) {
|
|
@@ -4,23 +4,25 @@ import { writeFile, mkdir } from 'fs/promises';
|
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
5
|
import { readConfig, writeConfig, CERTOPS_DIR, HOOKS_DIR } from '../../config.js';
|
|
6
6
|
import { domainHookPath, GLOBAL_HOOK } from '../../hooks.js';
|
|
7
|
-
|
|
7
|
+
import { BRAND } from '../../brand.js';
|
|
8
|
+
const UNIT_PATH = `/etc/systemd/system/${BRAND.serviceUnit}.service`;
|
|
9
|
+
const API_KEY_ENV = `${BRAND.envPrefix}_API_KEY`;
|
|
8
10
|
function buildUnitContent(apiKey) {
|
|
9
11
|
return `\
|
|
10
12
|
[Unit]
|
|
11
|
-
Description
|
|
13
|
+
Description=${BRAND.displayName} Certificate Monitor
|
|
12
14
|
After=network-online.target
|
|
13
15
|
Wants=network-online.target
|
|
14
16
|
|
|
15
17
|
[Service]
|
|
16
18
|
Type=simple
|
|
17
|
-
Environment
|
|
18
|
-
ExecStart=/usr/local/bin
|
|
19
|
+
Environment=${API_KEY_ENV}=${apiKey}
|
|
20
|
+
ExecStart=/usr/local/bin/${BRAND.binName} service run
|
|
19
21
|
Restart=on-failure
|
|
20
22
|
RestartSec=30
|
|
21
23
|
StandardOutput=journal
|
|
22
24
|
StandardError=journal
|
|
23
|
-
SyslogIdentifier
|
|
25
|
+
SyslogIdentifier=${BRAND.serviceUnit}
|
|
24
26
|
|
|
25
27
|
[Install]
|
|
26
28
|
WantedBy=multi-user.target
|
|
@@ -28,36 +30,35 @@ WantedBy=multi-user.target
|
|
|
28
30
|
}
|
|
29
31
|
const HOOK_TEMPLATE = `#!/usr/bin/env bash
|
|
30
32
|
# Available env vars:
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
33
|
+
# ${BRAND.envPrefix}_CERT_NAME — certificate name (e.g. *.example.com)
|
|
34
|
+
# ${BRAND.envPrefix}_DOMAIN — domain name (certName from API)
|
|
35
|
+
# ${BRAND.envPrefix}_FULLCHAIN_PATH — path to fullchain.pem
|
|
36
|
+
# ${BRAND.envPrefix}_CERT_PATH — path to cert.pem
|
|
37
|
+
# ${BRAND.envPrefix}_CHAIN_PATH — path to chain.pem
|
|
38
|
+
# ${BRAND.envPrefix}_KEY_PATH — path to privkey.pem
|
|
37
39
|
|
|
38
40
|
# Example: reload nginx after cert update
|
|
39
41
|
# systemctl reload nginx
|
|
40
42
|
`;
|
|
41
43
|
export const installCommand = new Command('install')
|
|
42
|
-
.description(
|
|
44
|
+
.description(`Install and configure the ${BRAND.displayName} background service`)
|
|
43
45
|
.action(async () => {
|
|
44
46
|
try {
|
|
45
47
|
if (process.getuid?.() !== 0) {
|
|
46
|
-
console.error(
|
|
47
|
-
console.error(
|
|
48
|
+
console.error(`Error: ${BRAND.binName} service install must run as root.`);
|
|
49
|
+
console.error(`\n sudo -E ${BRAND.binName} service install\n`);
|
|
48
50
|
process.exit(1);
|
|
49
51
|
}
|
|
50
|
-
console.log(
|
|
52
|
+
console.log(`\n${BRAND.displayName} Service Setup\n`);
|
|
51
53
|
const existing = await readConfig();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
validate: (v) => v.startsWith('sslpilot_') && v.length > 10
|
|
54
|
+
const apiKey = process.env[API_KEY_ENV] ?? await input({
|
|
55
|
+
message: `API key (${BRAND.keyPrefix}...):`,
|
|
56
|
+
validate: (v) => v.startsWith(BRAND.keyPrefix) && v.length > 10
|
|
56
57
|
? true
|
|
57
|
-
:
|
|
58
|
+
: `Key must start with ${BRAND.keyPrefix}`,
|
|
58
59
|
});
|
|
59
60
|
const apiUrl = await input({
|
|
60
|
-
message:
|
|
61
|
+
message: `API URL (leave blank for default ${BRAND.apiUrl}):`,
|
|
61
62
|
default: existing.apiUrl ?? '',
|
|
62
63
|
});
|
|
63
64
|
const intervalStr = await input({
|
|
@@ -80,12 +81,10 @@ export const installCommand = new Command('install')
|
|
|
80
81
|
watchDomains,
|
|
81
82
|
maxDownloadRetries: existing.maxDownloadRetries,
|
|
82
83
|
};
|
|
83
|
-
// Write config + create directories
|
|
84
84
|
await writeConfig(config);
|
|
85
85
|
await mkdir(CERTOPS_DIR, { recursive: true });
|
|
86
86
|
await mkdir(HOOKS_DIR, { recursive: true });
|
|
87
|
-
console.log(
|
|
88
|
-
// Create hook stubs only if they don't exist yet
|
|
87
|
+
console.log(`\n✓ Config written to ${CERTOPS_DIR}/config.json`);
|
|
89
88
|
for (const domain of watchDomains) {
|
|
90
89
|
const hookFile = domainHookPath(domain);
|
|
91
90
|
try {
|
|
@@ -105,13 +104,12 @@ export const installCommand = new Command('install')
|
|
|
105
104
|
if (err.code !== 'EEXIST')
|
|
106
105
|
throw err;
|
|
107
106
|
}
|
|
108
|
-
// Write systemd unit
|
|
109
107
|
await writeFile(UNIT_PATH, buildUnitContent(apiKey), { encoding: 'utf8', mode: 0o600 });
|
|
110
108
|
console.log(`✓ Systemd unit written to ${UNIT_PATH}`);
|
|
111
109
|
for (const args of [
|
|
112
110
|
['daemon-reload'],
|
|
113
|
-
['enable',
|
|
114
|
-
['restart',
|
|
111
|
+
['enable', BRAND.serviceUnit],
|
|
112
|
+
['restart', BRAND.serviceUnit],
|
|
115
113
|
]) {
|
|
116
114
|
const r = spawnSync('systemctl', args, { stdio: ['inherit', 'inherit', 'pipe'], encoding: 'utf8' });
|
|
117
115
|
if (r.status !== 0) {
|
|
@@ -121,9 +119,9 @@ export const installCommand = new Command('install')
|
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
console.log('\n✓ Service enabled and started.\n');
|
|
124
|
-
console.log(
|
|
125
|
-
console.log(
|
|
126
|
-
console.log(
|
|
122
|
+
console.log(` Edit hooks in : ${HOOKS_DIR}/`);
|
|
123
|
+
console.log(` Check status : ${BRAND.binName} service status`);
|
|
124
|
+
console.log(` Follow logs : journalctl -u ${BRAND.serviceUnit} -f\n`);
|
|
127
125
|
}
|
|
128
126
|
catch (err) {
|
|
129
127
|
if (err.name === 'ExitPromptError')
|
|
@@ -4,17 +4,19 @@ import { readConfig } from '../../config.js';
|
|
|
4
4
|
import { readState, updateCertState } from '../../state.js';
|
|
5
5
|
import { runHooks } from '../../hooks.js';
|
|
6
6
|
import { CLI_VERSION } from '../../version.js';
|
|
7
|
+
import { BRAND } from '../../brand.js';
|
|
7
8
|
const BASE_RETRY_DELAY_MS = 2_000; // 2 s → 4 s → 8 s …
|
|
8
9
|
function sleep(ms) {
|
|
9
10
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
11
|
}
|
|
11
12
|
export async function resolveApiKey() {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const envKey = `${BRAND.envPrefix}_API_KEY`;
|
|
14
|
+
if (process.env[envKey])
|
|
15
|
+
return process.env[envKey];
|
|
14
16
|
try {
|
|
15
17
|
const { execSync } = await import('child_process');
|
|
16
|
-
const out = execSync(
|
|
17
|
-
const match = out.match(
|
|
18
|
+
const out = execSync(`systemctl show ${BRAND.serviceUnit} -p Environment --value`, { encoding: 'utf8' });
|
|
19
|
+
const match = out.match(new RegExp(`${BRAND.envPrefix}_API_KEY=(\\S+)`));
|
|
18
20
|
return match?.[1] ?? null;
|
|
19
21
|
}
|
|
20
22
|
catch {
|
|
@@ -68,11 +70,11 @@ export async function runRenewalCycle(apiKey, log) {
|
|
|
68
70
|
log(` key : ${paths.keyPath}`);
|
|
69
71
|
await updateCertState(cert.certName, cert.expiryDate ?? '');
|
|
70
72
|
await runHooks(cert.certName, {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
[`${BRAND.envPrefix}_DOMAIN`]: cert.certName,
|
|
74
|
+
[`${BRAND.envPrefix}_FULLCHAIN_PATH`]: paths.fullchainPath,
|
|
75
|
+
[`${BRAND.envPrefix}_CERT_PATH`]: paths.certPath,
|
|
76
|
+
[`${BRAND.envPrefix}_CHAIN_PATH`]: paths.chainPath,
|
|
77
|
+
[`${BRAND.envPrefix}_KEY_PATH`]: paths.keyPath,
|
|
76
78
|
}, log);
|
|
77
79
|
downloaded++;
|
|
78
80
|
lastError = undefined;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
|
|
3
|
+
import { BRAND } from './brand.js';
|
|
4
|
+
export const CERTOPS_DIR = BRAND.configDir;
|
|
4
5
|
export const HOOKS_DIR = join(CERTOPS_DIR, 'hooks');
|
|
5
6
|
const CONFIG_PATH = join(CERTOPS_DIR, 'config.json');
|
|
6
7
|
const DEFAULTS = {
|
package/dist/hooks.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import { access } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { HOOKS_DIR } from './config.js';
|
|
5
|
+
import { BRAND } from './brand.js';
|
|
5
6
|
const GLOBAL_HOOK = join(HOOKS_DIR, 'global.sh');
|
|
6
7
|
function domainHookPath(certName) {
|
|
7
8
|
const safe = certName.replace(/^\*\./, 'wildcard.');
|
|
@@ -33,7 +34,7 @@ async function execHook(scriptPath, vars) {
|
|
|
33
34
|
}
|
|
34
35
|
export async function runHooks(certName, vars, log) {
|
|
35
36
|
const domainHook = domainHookPath(certName);
|
|
36
|
-
const hookVars = { ...vars,
|
|
37
|
+
const hookVars = { ...vars, [`${BRAND.envPrefix}_CERT_NAME`]: certName };
|
|
37
38
|
if (await fileExists(domainHook)) {
|
|
38
39
|
log(` Running domain hook: ${domainHook}`);
|
|
39
40
|
try {
|
package/dist/index.js
CHANGED
|
@@ -3,20 +3,19 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { listCommand } from './commands/list.js';
|
|
4
4
|
import { downloadCommand } from './commands/download.js';
|
|
5
5
|
import { serviceCommand } from './commands/service/index.js';
|
|
6
|
+
import { CLI_VERSION } from './version.js';
|
|
7
|
+
import { BRAND } from './brand.js';
|
|
6
8
|
const program = new Command()
|
|
7
|
-
.name(
|
|
8
|
-
.description(
|
|
9
|
-
.version(
|
|
9
|
+
.name(BRAND.binName)
|
|
10
|
+
.description(`${BRAND.displayName} CLI — manage and auto-renew your SSL certificates`)
|
|
11
|
+
.version(CLI_VERSION)
|
|
10
12
|
.addHelpText('after', `
|
|
11
13
|
Examples:
|
|
12
|
-
|
|
13
|
-
sudo
|
|
14
|
-
sudo
|
|
15
|
-
sudo
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Documentation:
|
|
19
|
-
https://github.com/Vimosoftdev/ssl-checker-web/blob/main/SETUP.md
|
|
14
|
+
${BRAND.binName} list List all certificates
|
|
15
|
+
sudo ${BRAND.binName} download Pick and download interactively
|
|
16
|
+
sudo ${BRAND.binName} download '*.example.com' Download a specific domain
|
|
17
|
+
sudo ${BRAND.binName} service install Set up the auto-renewal service
|
|
18
|
+
${BRAND.binName} service status Check service status
|
|
20
19
|
`);
|
|
21
20
|
program.addCommand(listCommand);
|
|
22
21
|
program.addCommand(downloadCommand);
|