@gorlitzer-labs/comb 0.1.2 → 0.1.4
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 +35 -1
- package/package.json +1 -1
- package/src/realms.mjs +16 -6
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 |
|
|
@@ -65,6 +65,40 @@ Values are never printed, never passed as command-line arguments, never logged.
|
|
|
65
65
|
`comb get NAME --reveal` exists for when you genuinely need to read one, and
|
|
66
66
|
refuses without the flag.
|
|
67
67
|
|
|
68
|
+
## Tokens in MCP configs
|
|
69
|
+
|
|
70
|
+
An HTTP MCP server with a bearer token tends to end up as a plaintext
|
|
71
|
+
`"headers": {"Authorization": "Bearer …"}` in `~/.claude.json` — a file every
|
|
72
|
+
agent session reads, and that gets pasted into conversations. Claude Code can
|
|
73
|
+
fetch headers from a command instead (`headersHelper`), so the token can stay
|
|
74
|
+
in comb:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
comb set AGENT_COORD_REMOTE_TOKEN --url "http://my-box:8765 (agent-coord-remote)"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
#!/bin/sh
|
|
82
|
+
# ~/.claude/bin/agent-coord-remote-headers.sh (chmod +x)
|
|
83
|
+
export PATH="$HOME/Library/pnpm:/opt/homebrew/bin:/usr/bin:/bin" # Claude may not have your shell PATH
|
|
84
|
+
exec comb run --with AGENT_COORD_REMOTE_TOKEN -- \
|
|
85
|
+
sh -c 'printf "{\"Authorization\":\"Bearer %s\"}" "$AGENT_COORD_REMOTE_TOKEN"'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```jsonc
|
|
89
|
+
// ~/.claude.json → mcpServers
|
|
90
|
+
"agent-coord-remote": {
|
|
91
|
+
"type": "http",
|
|
92
|
+
"url": "http://my-box:8765/mcp",
|
|
93
|
+
"headersHelper": "/Users/you/.claude/bin/agent-coord-remote-headers.sh"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The helper prints the headers as JSON and Claude reads them at connect time. The
|
|
98
|
+
token never sits in the config, and `comb run` keeps it out of argv. Moving a token
|
|
99
|
+
that was already in plaintext is not the same as rotating it: run `comb audit`,
|
|
100
|
+
and if it shows up as LEAKED, rotate it.
|
|
101
|
+
|
|
68
102
|
## `comb audit`
|
|
69
103
|
|
|
70
104
|
This is the half no vault does, and the half that makes manual rotation
|
package/package.json
CHANGED
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
|
|