certops 1.0.15 → 1.1.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/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 +8 -6
- 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/dist/version.js +13 -12
- package/package.json +6 -3
- package/scripts/postinstall.cjs +72 -0
- package/scripts/preuninstall.cjs +15 -0
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
|
}
|
|
@@ -7,7 +7,13 @@ import { domainHookPath, GLOBAL_HOOK } from '../../hooks.js';
|
|
|
7
7
|
import { BRAND } from '../../brand.js';
|
|
8
8
|
const UNIT_PATH = `/etc/systemd/system/${BRAND.serviceUnit}.service`;
|
|
9
9
|
const API_KEY_ENV = `${BRAND.envPrefix}_API_KEY`;
|
|
10
|
+
function resolveBinPath() {
|
|
11
|
+
// process.argv[1] is the actual path of the running certops script
|
|
12
|
+
return process.argv[1] ?? `/usr/local/bin/${BRAND.binName}`;
|
|
13
|
+
}
|
|
10
14
|
function buildUnitContent(apiKey) {
|
|
15
|
+
const binPath = resolveBinPath();
|
|
16
|
+
const nodePath = process.execPath;
|
|
11
17
|
return `\
|
|
12
18
|
[Unit]
|
|
13
19
|
Description=${BRAND.displayName} Certificate Monitor
|
|
@@ -17,7 +23,7 @@ Wants=network-online.target
|
|
|
17
23
|
[Service]
|
|
18
24
|
Type=simple
|
|
19
25
|
Environment=${API_KEY_ENV}=${apiKey}
|
|
20
|
-
ExecStart
|
|
26
|
+
ExecStart=${nodePath} ${binPath} service run
|
|
21
27
|
Restart=on-failure
|
|
22
28
|
RestartSec=30
|
|
23
29
|
StandardOutput=journal
|
|
@@ -57,10 +63,6 @@ export const installCommand = new Command('install')
|
|
|
57
63
|
? true
|
|
58
64
|
: `Key must start with ${BRAND.keyPrefix}`,
|
|
59
65
|
});
|
|
60
|
-
const apiUrl = await input({
|
|
61
|
-
message: `API URL (leave blank for default ${BRAND.apiUrl}):`,
|
|
62
|
-
default: existing.apiUrl ?? '',
|
|
63
|
-
});
|
|
64
66
|
const intervalStr = await input({
|
|
65
67
|
message: 'Check interval (hours):',
|
|
66
68
|
default: String(existing.checkIntervalHours),
|
|
@@ -75,7 +77,7 @@ export const installCommand = new Command('install')
|
|
|
75
77
|
.map(d => d.trim())
|
|
76
78
|
.filter(Boolean);
|
|
77
79
|
const config = {
|
|
78
|
-
apiUrl: apiUrl
|
|
80
|
+
apiUrl: existing.apiUrl,
|
|
79
81
|
renewalThresholdDays: existing.renewalThresholdDays,
|
|
80
82
|
checkIntervalHours: Number(intervalStr),
|
|
81
83
|
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
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
function isPackageJson(value) {
|
|
5
|
-
return (typeof value === 'object' &&
|
|
6
|
-
value !== null &&
|
|
7
|
-
'version' in value &&
|
|
8
|
-
typeof value.version === 'string');
|
|
9
|
-
}
|
|
10
4
|
function readCliVersion() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
// Injected at compile time by Bun build --define
|
|
6
|
+
if (process.env.CLI_VERSION)
|
|
7
|
+
return process.env.CLI_VERSION;
|
|
8
|
+
// JS mode (npm package with dist/): read from package.json
|
|
9
|
+
try {
|
|
10
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const path = join(dir, '..', 'package.json');
|
|
12
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
13
|
+
return raw.version;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return '0.0.0';
|
|
17
|
+
}
|
|
17
18
|
}
|
|
18
19
|
export const CLI_VERSION = readCliVersion();
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "certops",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "SSL Pilot CLI — download and manage your SSL certificates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
-
"dist"
|
|
7
|
+
"dist",
|
|
8
|
+
"scripts"
|
|
8
9
|
],
|
|
9
10
|
"bin": {
|
|
10
11
|
"certops": "./dist/index.js"
|
|
@@ -12,7 +13,9 @@
|
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsc",
|
|
14
15
|
"dev": "tsx src/index.ts",
|
|
15
|
-
"typecheck": "tsc --noEmit"
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
18
|
+
"preuninstall": "node scripts/preuninstall.cjs"
|
|
16
19
|
},
|
|
17
20
|
"publishConfig": {
|
|
18
21
|
"access": "public"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { createWriteStream, chmodSync, existsSync, unlinkSync } = require('fs')
|
|
5
|
+
const { get } = require('https')
|
|
6
|
+
const { join } = require('path')
|
|
7
|
+
|
|
8
|
+
const { name, version } = require('../package.json')
|
|
9
|
+
const REPO = 'Vimosoftdev/ssl-checker-web'
|
|
10
|
+
const BIN_NAME = 'certops'
|
|
11
|
+
const INSTALL_DIR = '/usr/local/bin'
|
|
12
|
+
const INSTALL_PATH = join(INSTALL_DIR, BIN_NAME)
|
|
13
|
+
|
|
14
|
+
const platformMap = { linux: 'linux', darwin: 'darwin' }
|
|
15
|
+
const archMap = { x64: 'x64', arm64: 'arm64' }
|
|
16
|
+
const plat = platformMap[process.platform]
|
|
17
|
+
const arc = archMap[process.arch]
|
|
18
|
+
|
|
19
|
+
if (!plat || !arc) {
|
|
20
|
+
console.warn(`[certops] Unsupported platform ${process.platform}-${process.arch}. Skipping binary install.`)
|
|
21
|
+
console.warn(`[certops] Install manually: https://github.com/${REPO}/releases/latest`)
|
|
22
|
+
process.exit(0)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0
|
|
26
|
+
|
|
27
|
+
if (!isRoot) {
|
|
28
|
+
console.warn(`[certops] Not running as root — binary not installed to ${INSTALL_PATH}.`)
|
|
29
|
+
console.warn(`[certops] To install system-wide: sudo npm install -g certops`)
|
|
30
|
+
console.warn(`[certops] Or: curl -fsSL https://github.com/${REPO}/releases/latest/download/install.sh | sudo bash`)
|
|
31
|
+
process.exit(0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const assetName = `${BIN_NAME}-${plat}-${arc}`
|
|
35
|
+
const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`
|
|
36
|
+
|
|
37
|
+
console.log(`[certops] Installing v${version} binary for ${plat}-${arc}…`)
|
|
38
|
+
console.log(`[certops] Downloading ${downloadUrl}`)
|
|
39
|
+
|
|
40
|
+
downloadFile(downloadUrl, INSTALL_PATH)
|
|
41
|
+
.then(() => {
|
|
42
|
+
chmodSync(INSTALL_PATH, 0o755)
|
|
43
|
+
console.log(`[certops] ✓ Installed to ${INSTALL_PATH}`)
|
|
44
|
+
})
|
|
45
|
+
.catch(err => {
|
|
46
|
+
// clean up partial download
|
|
47
|
+
try { if (existsSync(INSTALL_PATH)) unlinkSync(INSTALL_PATH) } catch {}
|
|
48
|
+
console.error(`[certops] Binary install failed: ${err.message}`)
|
|
49
|
+
console.error(`[certops] Fallback: curl -fsSL https://github.com/${REPO}/releases/latest/download/install.sh | sudo bash`)
|
|
50
|
+
// non-fatal — npm install still succeeds
|
|
51
|
+
process.exit(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
function downloadFile(url, dest) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
function fetch(url) {
|
|
57
|
+
get(url, res => {
|
|
58
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
59
|
+
return fetch(res.headers.location)
|
|
60
|
+
}
|
|
61
|
+
if (res.statusCode !== 200) {
|
|
62
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${url}`))
|
|
63
|
+
}
|
|
64
|
+
const file = createWriteStream(dest)
|
|
65
|
+
res.pipe(file)
|
|
66
|
+
file.on('finish', () => file.close(resolve))
|
|
67
|
+
file.on('error', err => { unlinkSync(dest); reject(err) })
|
|
68
|
+
}).on('error', reject)
|
|
69
|
+
}
|
|
70
|
+
fetch(url)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { unlinkSync, existsSync } = require('fs')
|
|
5
|
+
|
|
6
|
+
const INSTALL_PATH = '/usr/local/bin/certops'
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (existsSync(INSTALL_PATH)) {
|
|
10
|
+
unlinkSync(INSTALL_PATH)
|
|
11
|
+
console.log(`[certops] ✓ Removed binary from ${INSTALL_PATH}`)
|
|
12
|
+
}
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.warn(`[certops] Could not remove ${INSTALL_PATH}: ${err.message}`)
|
|
15
|
+
}
|