@gorlitzer-labs/comb 0.1.1 → 0.1.3
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 +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +3 -1
- package/src/realms.mjs +16 -6
- package/src/update-check.mjs +42 -0
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ matters. Everything else is recoverable; that is not. Back it up somewhere real.
|
|
|
55
55
|
|---|---|
|
|
56
56
|
| `comb init` | create the age key and the encrypted store |
|
|
57
57
|
| `comb set <NAME> [--url <where>]` | take a value without echoing it, and store it |
|
|
58
|
-
| `comb ls` | names,
|
|
58
|
+
| `comb ls` | names, age and notes — never values |
|
|
59
59
|
| `comb run --with A,B -- <cmd>` | run a command with those secrets in its environment |
|
|
60
60
|
| `comb rotate <NAME>` | tell you where to go, then take the new value |
|
|
61
61
|
| `comb audit` | which secrets leaked into transcripts and shell history |
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { createInterface } from "node:readline";
|
|
|
23
23
|
import * as store from "./backends/sops.mjs";
|
|
24
24
|
import * as realms from "./realms.mjs";
|
|
25
25
|
import { scan, defaultTargets } from "./audit.mjs";
|
|
26
|
+
import { VERSION, checkAndNotify } from "./update-check.mjs";
|
|
26
27
|
|
|
27
28
|
const B = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
28
29
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
@@ -80,6 +81,7 @@ function help() {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
84
|
+
if (process.stdout.isTTY) checkAndNotify();
|
|
83
85
|
const flags = {}; const pos = [];
|
|
84
86
|
for (let i = 0; i < rest.length; i++) {
|
|
85
87
|
const a = rest[i];
|
|
@@ -234,7 +236,7 @@ try {
|
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
case undefined: case "help": case "--help": case "-h": help(); break;
|
|
237
|
-
case "version": case "--version": case "-v": say(
|
|
239
|
+
case "version": case "--version": case "-v": say(`comb ${VERSION}`); break;
|
|
238
240
|
default: say(red(`unknown command: ${cmd}`)); help(); process.exit(2);
|
|
239
241
|
}
|
|
240
242
|
} catch (e) {
|
package/src/realms.mjs
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// be rotated, because that realm can no longer decrypt anything, old copies
|
|
12
12
|
// included (its key never changes what the current ciphertext is encrypted to).
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, mkdtempSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
16
17
|
import { combHome, storePath, keyPath } from "./backends/sops.mjs";
|
|
17
18
|
|
|
18
19
|
export const recipientsDir = (home = combHome()) => join(home, "recipients");
|
|
@@ -54,11 +55,20 @@ export function rekeyStore(home = combHome()) {
|
|
|
54
55
|
// Decrypt to JSON in memory, then re-encrypt to the new recipient set.
|
|
55
56
|
const dec = spawnSync("sops", ["--decrypt", "--output-type", "json", storePath(home)], { encoding: "utf-8", env });
|
|
56
57
|
if (dec.status !== 0) throw new Error(`could not decrypt to re-key: ${dec.stderr?.trim()}`);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// Encrypt from a private temp FILE rather than `/dev/stdin`: /dev/stdin is not
|
|
59
|
+
// openable in non-interactive contexts (CI, some sandboxes), which broke re-key.
|
|
60
|
+
const dir = mkdtempSync(join(tmpdir(), "comb-rekey-"));
|
|
61
|
+
const plain = join(dir, "store.json");
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(plain, dec.stdout, { mode: 0o600 });
|
|
64
|
+
const enc = spawnSync("sops",
|
|
65
|
+
["--encrypt", "--age", recipients.join(","), "--input-type", "json", "--output-type", "yaml", plain],
|
|
66
|
+
{ encoding: "utf-8", env });
|
|
67
|
+
if (enc.status !== 0) throw new Error(`could not re-encrypt: ${enc.stderr?.trim()}`);
|
|
68
|
+
writeFileSync(storePath(home), enc.stdout);
|
|
69
|
+
} finally {
|
|
70
|
+
rmSync(dir, { recursive: true, force: true }); // wipe the plaintext temp
|
|
71
|
+
}
|
|
62
72
|
return recipients.length;
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Throttled npm-registry update check — shared shape across the Seldon tools.
|
|
2
|
+
// Reads this package's own version, checks the registry at most once a day
|
|
3
|
+
// (cached under ~/.seldon), and prints a one-line notice to stderr if a newer
|
|
4
|
+
// version is out. Never blocks meaningfully, never throws.
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PKG = JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8"));
|
|
13
|
+
export const VERSION = PKG.version;
|
|
14
|
+
const NAME = PKG.name;
|
|
15
|
+
const SHORT = NAME.includes("/") ? NAME.split("/").pop() : NAME;
|
|
16
|
+
const CACHE = join(homedir(), ".seldon", `update-${SHORT}.json`);
|
|
17
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
const newer = (a, b) => {
|
|
20
|
+
const pa = String(a).split(".").map(Number), pb = String(b).split(".").map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d > 0; }
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
const read = () => { try { return JSON.parse(readFileSync(CACHE, "utf8")); } catch { return null; } };
|
|
25
|
+
const write = (o) => { try { mkdirSync(dirname(CACHE), { recursive: true }); writeFileSync(CACHE, JSON.stringify(o)); } catch { /* ignore */ } };
|
|
26
|
+
|
|
27
|
+
/** Check (throttled to once/day) and print an update notice to stderr.
|
|
28
|
+
* Safe to call on every run — silent when current, offline, or npm is absent. */
|
|
29
|
+
export function checkAndNotify() {
|
|
30
|
+
try {
|
|
31
|
+
const c = read();
|
|
32
|
+
let latest = c?.latest;
|
|
33
|
+
if (!c || Date.now() - c.at > DAY) {
|
|
34
|
+
try {
|
|
35
|
+
latest = execFileSync("npm", ["view", NAME, "version"], { timeout: 3000, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
36
|
+
if (/^\d+\.\d+\.\d+/.test(latest)) write({ at: Date.now(), latest });
|
|
37
|
+
} catch { /* offline / npm unavailable — skip */ }
|
|
38
|
+
}
|
|
39
|
+
if (latest && newer(latest, VERSION))
|
|
40
|
+
process.stderr.write(`\x1b[33m ${SHORT} ${latest} is available\x1b[0m \x1b[2m— you have ${VERSION}. Update: \x1b[36mseldon install ${SHORT}\x1b[0m\x1b[2m (or npm i -g ${NAME})\x1b[0m\n`);
|
|
41
|
+
} catch { /* never break the CLI */ }
|
|
42
|
+
}
|