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 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 fetch(`${base}/api/cli${path}`, {
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 fetch(`${base}/api/cli${path}`, {
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
- process.stderr.write(`\nError: ${err.message}\n\n`);
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('Error: certops service configure must run as root.');
11
- console.error('\n sudo sp service configure\n');
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('\nCertOps — Update Service Config\n');
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('\n✓ Config updated at /etc/certops/config.json');
48
- const result = spawnSync('systemctl', ['restart', 'certops'], {
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(' Run: sudo sp service start\n\n');
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 = (msg) => process.stdout.write(` ${msg}\n`);
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
- function log(msg) {
6
- process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
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.trim() || undefined,
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(`/etc/certops requires root. Re-run:\n\n`);
49
- process.stderr.write(` sudo certops download '${certName}'\n\n`);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "certops",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "SSL Pilot CLI — download and manage your SSL certificates",
5
5
  "type": "module",
6
6
  "files": [