@keynv/cli 0.1.0-rc.22 → 0.1.0-rc.23
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 +72 -50
- package/dist/index.js +121 -188
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,80 +1,88 @@
|
|
|
1
1
|
# @keynv/cli
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Self-hosted secrets vault for teams and AI-assisted development.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@keynv/cli)
|
|
6
6
|
[](https://github.com/keynv-labs/keynv/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
! zsh history 5 likely secrets across 1 file
|
|
16
|
-
! Claude Code transcripts 62,306 likely secrets across 73 files
|
|
17
|
-
· Cursor logs clean
|
|
18
|
-
|
|
19
|
-
Total: 62,311 likely secrets across 74 files.
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`doctor`, `scrub`, `shell`, and `watch` are fully local — no account, no server,
|
|
23
|
-
no network calls. Match previews are bounded to 3 characters; raw secret values
|
|
24
|
-
never appear in output.
|
|
8
|
+
Store your API keys, database URLs, and other secrets in one vault — hosted on
|
|
9
|
+
[keynv.dev](https://keynv.dev) or self-hosted — and reference them everywhere by
|
|
10
|
+
alias (`@project.env.key`) instead of the real value. `keynv` resolves aliases
|
|
11
|
+
into your app at runtime, so the real values never touch your code, your shell
|
|
12
|
+
history, or your AI agent's transcripts. Your agent sees
|
|
13
|
+
`@billing.prod.stripe_key`, never the key itself.
|
|
25
14
|
|
|
26
15
|
## Install
|
|
27
16
|
|
|
28
17
|
```bash
|
|
29
18
|
npm install -g @keynv/cli
|
|
30
|
-
keynv doctor
|
|
31
19
|
```
|
|
32
20
|
|
|
33
|
-
Prefer a standalone binary over Node? Every
|
|
21
|
+
Node 20+. Prefer a standalone binary over Node? Every
|
|
34
22
|
[release](https://github.com/keynv-labs/keynv/releases) publishes signed
|
|
35
23
|
executables for macOS / Linux / Windows with a `SHA256SUMS` manifest.
|
|
36
24
|
|
|
37
25
|
## Quick start
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
# 1. Find out where you're leaking (scan only — nothing is rewritten)
|
|
41
|
-
keynv doctor
|
|
27
|
+
### Option A · Use keynv.dev (hosted — fastest)
|
|
42
28
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
keynv scrub
|
|
29
|
+
1. Create a free account at [keynv.dev/register](https://keynv.dev/register).
|
|
30
|
+
2. Connect and set up your project:
|
|
46
31
|
|
|
47
|
-
|
|
48
|
-
keynv
|
|
49
|
-
|
|
32
|
+
```bash
|
|
33
|
+
keynv # pick "keynv.dev", then "Set up this project"
|
|
34
|
+
```
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
keynv watch start
|
|
53
|
-
keynv watch status
|
|
54
|
-
```
|
|
36
|
+
### Option B · Self-host
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
38
|
+
Run your own server + panel in ~15 minutes — see the
|
|
39
|
+
[Coolify walkthrough](https://github.com/keynv-labs/keynv/blob/main/deploy/COOLIFY.md)
|
|
40
|
+
or [Docker Compose guide](https://github.com/keynv-labs/keynv/blob/main/deploy/README.md).
|
|
41
|
+
Then run `keynv`, choose **Self-hosted server**, and enter your API URL.
|
|
60
42
|
|
|
61
|
-
|
|
43
|
+
> On a headless Linux box with no OS keychain (no libsecret), set
|
|
44
|
+
> `KEYNV_DISABLE_KEYCHAIN=1` before connecting — credentials then persist to an
|
|
45
|
+
> encrypted file instead of the system keychain.
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
### Then run your app — the way you already do
|
|
48
|
+
|
|
49
|
+
Setup imports your existing `.env` files into the vault and writes a committable
|
|
50
|
+
`.keynv.env` that maps your environment variables to aliases. It also wraps your
|
|
51
|
+
`package.json` scripts, so you just run them normally:
|
|
68
52
|
|
|
69
53
|
```bash
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
keynv secret create @demo.dev.api_key --value 'whatever'
|
|
73
|
-
keynv exec -- npm run dev # aliases resolved into the child, redacted from output
|
|
54
|
+
npm run dev # secrets injected automatically — no `keynv exec` to type
|
|
55
|
+
npm test
|
|
74
56
|
```
|
|
75
57
|
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
`.keynv.env` is safe to commit — it holds references, not values:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
OPENAI_API_KEY=@myapp.dev.openai-key
|
|
62
|
+
DATABASE_URL=@myapp.prod.db-url
|
|
63
|
+
NODE_ENV=development # plain literals pass through unchanged
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Behind the scenes your wrapped script runs `keynv exec -- <your command>`, which
|
|
67
|
+
resolves each `@alias` against the vault and forks your command with the real
|
|
68
|
+
values in a subprocess your editor and AI agent can't read — redacting them from
|
|
69
|
+
output. **You never type `keynv exec` yourself.**
|
|
70
|
+
|
|
71
|
+
Teams get project-scoped roles (owner / admin / developer / reader), per-secret
|
|
72
|
+
access, rotation, and a tamper-evident audit log — all managed from the CLI or
|
|
73
|
+
the web panel.
|
|
74
|
+
|
|
75
|
+
## Common commands
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
keynv # interactive TUI (projects, secrets, members)
|
|
79
|
+
keynv secret create # add a secret to the vault
|
|
80
|
+
keynv secret get @a.dev.key --copy # copy a value to the clipboard without printing it
|
|
81
|
+
keynv exec -- <cmd> # run a one-off command with aliases resolved
|
|
82
|
+
keynv member list <project> # who has access to a project
|
|
83
|
+
keynv whoami # who am I logged in as
|
|
84
|
+
keynv --help # full command list
|
|
85
|
+
```
|
|
78
86
|
|
|
79
87
|
## Use with Claude Code / Cursor (MCP)
|
|
80
88
|
|
|
@@ -83,11 +91,25 @@ never see the value. See the
|
|
|
83
91
|
[AI setup guide](https://github.com/keynv-labs/keynv/blob/main/docs/ai-setup.md)
|
|
84
92
|
for wiring it into Claude Code, Cursor, and other MCP clients.
|
|
85
93
|
|
|
94
|
+
## Also: clean up leaks you already have
|
|
95
|
+
|
|
96
|
+
Secrets already sitting in your shell history or AI transcripts? These commands
|
|
97
|
+
run entirely locally — no server or login needed:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
keynv doctor # scan for leaked secrets (read-only, no network)
|
|
101
|
+
keynv scrub # clean them up, atomically, with backups
|
|
102
|
+
keynv watch start # scrub live AI-agent sessions in real time
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Match previews are bounded to 3 characters; raw secret values never appear in
|
|
106
|
+
output.
|
|
107
|
+
|
|
86
108
|
## Documentation
|
|
87
109
|
|
|
88
110
|
- [Getting started](https://github.com/keynv-labs/keynv/blob/main/docs/getting-started.md)
|
|
89
111
|
- [AI setup (Claude Code / Cursor / MCP)](https://github.com/keynv-labs/keynv/blob/main/docs/ai-setup.md)
|
|
90
|
-
- [
|
|
112
|
+
- [Coolify deploy](https://github.com/keynv-labs/keynv/blob/main/deploy/COOLIFY.md)
|
|
91
113
|
- [Threat model](https://github.com/keynv-labs/keynv/blob/main/docs/02-threat-model.md)
|
|
92
114
|
- [Full README](https://github.com/keynv-labs/keynv#readme)
|
|
93
115
|
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
-
import { createHash, randomBytes } from 'crypto';
|
|
4
|
-
import { readFileSync, existsSync, writeFileSync, statSync, realpathSync, readdirSync, renameSync, mkdirSync, rmSync, constants as constants$1, lstatSync } from 'fs';
|
|
5
|
-
import { homedir, hostname, platform } from 'os';
|
|
6
3
|
import * as nodePath from 'path';
|
|
7
4
|
import { relative, join, isAbsolute, resolve, sep, dirname, basename } from 'path';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import { readFileSync, existsSync, writeFileSync, statSync, realpathSync, readdirSync, renameSync, mkdirSync, rmSync, constants as constants$1, lstatSync } from 'fs';
|
|
7
|
+
import { homedir, hostname, platform } from 'os';
|
|
8
8
|
import { mkdir, writeFile, readFile, unlink, stat, rm, access, constants, appendFile, readdir, open, rename, copyFile } from 'fs/promises';
|
|
9
9
|
import { createConnection, createServer } from 'net';
|
|
10
10
|
import { styleText, stripVTControlCharacters } from 'util';
|
|
@@ -1070,16 +1070,17 @@ var require_lib = __commonJS({
|
|
|
1070
1070
|
var VERSION, AGENT;
|
|
1071
1071
|
var init_version = __esm({
|
|
1072
1072
|
"src/version.ts"() {
|
|
1073
|
-
VERSION = "0.1.0-rc.
|
|
1073
|
+
VERSION = "0.1.0-rc.23" ;
|
|
1074
1074
|
AGENT = `keynv-cli/${VERSION}`;
|
|
1075
1075
|
}
|
|
1076
1076
|
});
|
|
1077
1077
|
|
|
1078
1078
|
// src/client/defaults.ts
|
|
1079
|
-
var DEFAULT_SERVER_URL;
|
|
1079
|
+
var DEFAULT_SERVER_URL, DEFAULT_WEB_URL;
|
|
1080
1080
|
var init_defaults = __esm({
|
|
1081
1081
|
"src/client/defaults.ts"() {
|
|
1082
1082
|
DEFAULT_SERVER_URL = "https://api.keynv.dev";
|
|
1083
|
+
DEFAULT_WEB_URL = "https://keynv.dev";
|
|
1083
1084
|
}
|
|
1084
1085
|
});
|
|
1085
1086
|
|
|
@@ -1227,6 +1228,14 @@ var init_types2 = __esm({
|
|
|
1227
1228
|
NONCE_BYTES = 24;
|
|
1228
1229
|
}
|
|
1229
1230
|
});
|
|
1231
|
+
function loadSodiumModule() {
|
|
1232
|
+
try {
|
|
1233
|
+
return moduleRequire("libsodium-wrappers");
|
|
1234
|
+
} catch {
|
|
1235
|
+
const runtimeRequire = createRequire(join(process.cwd(), "package.json"));
|
|
1236
|
+
return runtimeRequire("libsodium-wrappers");
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1230
1239
|
function loadSodium() {
|
|
1231
1240
|
if (!readyPromise) {
|
|
1232
1241
|
readyPromise = sodium.ready.then(() => sodium);
|
|
@@ -1240,11 +1249,11 @@ function zero(buf) {
|
|
|
1240
1249
|
}
|
|
1241
1250
|
buf.fill(0);
|
|
1242
1251
|
}
|
|
1243
|
-
var
|
|
1252
|
+
var moduleRequire, sodium, readyPromise;
|
|
1244
1253
|
var init_sodium = __esm({
|
|
1245
1254
|
"../../packages/core/dist/crypto/sodium.js"() {
|
|
1246
|
-
|
|
1247
|
-
sodium =
|
|
1255
|
+
moduleRequire = createRequire(import.meta.url);
|
|
1256
|
+
sodium = loadSodiumModule();
|
|
1248
1257
|
readyPromise = null;
|
|
1249
1258
|
}
|
|
1250
1259
|
});
|
|
@@ -5609,8 +5618,8 @@ function legacyPath() {
|
|
|
5609
5618
|
}
|
|
5610
5619
|
function loadEntryCtor() {
|
|
5611
5620
|
if (!EntryCtor) {
|
|
5612
|
-
const
|
|
5613
|
-
EntryCtor =
|
|
5621
|
+
const require2 = createRequire(import.meta.url);
|
|
5622
|
+
EntryCtor = require2("@napi-rs/keyring").Entry;
|
|
5614
5623
|
}
|
|
5615
5624
|
return EntryCtor;
|
|
5616
5625
|
}
|
|
@@ -6345,8 +6354,8 @@ When the user asks about secrets or env vars, follow this decision tree:
|
|
|
6345
6354
|
|---|---|
|
|
6346
6355
|
| "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
|
|
6347
6356
|
| "I have a \`.env\`, migrate it" | Run \`keynv\` and choose **Set up this project** |
|
|
6348
|
-
| "Run the app / dev server / tests" |
|
|
6349
|
-
| "Show me the value of X" | \`keynv secret get @alias --copy\`
|
|
6357
|
+
| "Run the app / dev server / tests" | Run the project's script as usual (e.g. \`npm run dev\`) \u2014 setup already wrapped it with keynv. For a one-off command that isn't a package.json script, use \`keynv exec -- <cmd>\`. |
|
|
6358
|
+
| "Show me the value of X" | Don't reveal it. \`keynv secret get @alias --copy\` puts the value on the clipboard without printing it. Bare \`keynv secret get\` refuses to print in a non-interactive context; \`--reveal\` forces raw stdout \u2014 avoid it in agent contexts. |
|
|
6350
6359
|
| "Rotate this key" | \`keynv secret rotate @alias\` |
|
|
6351
6360
|
| "Add a new env var to the project" | Edit \`.keynv.env\` and add \`NAME=@project.env.key\` (after creating the secret with \`keynv secret create\`) |
|
|
6352
6361
|
| "Who has access?" | \`keynv member list <project>\` |
|
|
@@ -9192,6 +9201,15 @@ var init_script_wrap = __esm({
|
|
|
9192
9201
|
"forever",
|
|
9193
9202
|
"nodemon",
|
|
9194
9203
|
"concurrently",
|
|
9204
|
+
// Monorepo / task runners — wrapping the orchestrator injects secrets into
|
|
9205
|
+
// every child task it spawns, so `npm run dev` still needs no `keynv exec`.
|
|
9206
|
+
"turbo",
|
|
9207
|
+
"nx",
|
|
9208
|
+
"lerna",
|
|
9209
|
+
"make",
|
|
9210
|
+
"npm-run-all",
|
|
9211
|
+
"run-p",
|
|
9212
|
+
"run-s",
|
|
9195
9213
|
"pytest",
|
|
9196
9214
|
"python",
|
|
9197
9215
|
"python3",
|
|
@@ -9300,10 +9318,20 @@ async function runInitFlow(client, opts) {
|
|
|
9300
9318
|
const envFiles = findEnvFilesRecursive(root.path);
|
|
9301
9319
|
const intoExisting = hasExistingKeynvEnv(root.path);
|
|
9302
9320
|
if (envFiles.length === 0 && !intoExisting) {
|
|
9303
|
-
|
|
9304
|
-
|
|
9321
|
+
Se(
|
|
9322
|
+
[
|
|
9323
|
+
`No .env files found in ${root.path} (scanned root and subdirectories),`,
|
|
9324
|
+
"so there is nothing to migrate yet. To start from scratch:",
|
|
9325
|
+
"",
|
|
9326
|
+
" 1. keynv secret create \u2014 add your first secret to the vault",
|
|
9327
|
+
" 2. add its alias to .keynv.env, e.g. OPENAI_API_KEY=@myapp.dev.openai-key",
|
|
9328
|
+
" 3. run your app normally \u2014 keynv injects the real value",
|
|
9329
|
+
"",
|
|
9330
|
+
'Or re-run `keynv` \u2192 "Set up this project" once you have a .env to import.'
|
|
9331
|
+
].join("\n"),
|
|
9332
|
+
"Nothing to migrate yet"
|
|
9305
9333
|
);
|
|
9306
|
-
ye("
|
|
9334
|
+
ye("Create your first secret with `keynv secret create`.");
|
|
9307
9335
|
return { exitCode: 0 };
|
|
9308
9336
|
}
|
|
9309
9337
|
Se(
|
|
@@ -9442,7 +9470,7 @@ ${detail}`
|
|
|
9442
9470
|
const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.composite);
|
|
9443
9471
|
const selectedComposites = unwrap(
|
|
9444
9472
|
await ve({
|
|
9445
|
-
message: "
|
|
9473
|
+
message: "Which keys are secrets? (pre-selected by keynv \u2014 press Enter to accept; unchecked stay as literals)",
|
|
9446
9474
|
options: choices.map((c2) => ({
|
|
9447
9475
|
value: c2.composite,
|
|
9448
9476
|
label: c2.label,
|
|
@@ -9468,19 +9496,21 @@ ${detail}`
|
|
|
9468
9496
|
const writeDirs = [...new Set(plan.resolved.map((r) => r.source.containingDir))];
|
|
9469
9497
|
const writeDirsLines = writeDirs.map((d2) => ` ${relFromRoot(root.path, d2)}`).join("\n");
|
|
9470
9498
|
const renameLine = plan.renamed.length > 0 ? `Renamed vault keys: ${plan.renamed.length} (to avoid cross-app collisions)` : "";
|
|
9499
|
+
const scriptWrapLine = scriptWrapSelection.length > 0 ? `package.json: wrap ${scriptWrapSelection.length} script(s) with \`keynv exec\` \u2014 ${scriptWrapSelection.join(", ")}` : "package.json: not modified (no wrappable scripts)";
|
|
9471
9500
|
const planSummary = [
|
|
9472
9501
|
`Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
|
|
9473
9502
|
`Environments: ${distinctEnvs.join(", ")}`,
|
|
9474
9503
|
"Per-env breakdown:",
|
|
9475
9504
|
perEnvCounts,
|
|
9476
|
-
|
|
9477
|
-
".keynv.env files: will be written under",
|
|
9505
|
+
".keynv.env files: written under (safe to commit \u2014 aliases, not values)",
|
|
9478
9506
|
writeDirsLines,
|
|
9479
|
-
|
|
9507
|
+
scriptWrapLine,
|
|
9508
|
+
"AGENTS.md: write/update keynv guidance for AI agents at the project root",
|
|
9509
|
+
".env files: renamed to .env.backup after their values are uploaded",
|
|
9480
9510
|
renameLine,
|
|
9481
9511
|
opts.dryRun ? "Dry-run: no changes will be made." : ""
|
|
9482
9512
|
].filter(Boolean).join("\n");
|
|
9483
|
-
Se(planSummary, "About to apply");
|
|
9513
|
+
Se(planSummary, "About to apply \u2014 one confirmation applies all of the below");
|
|
9484
9514
|
const proceed = unwrap(await ue({ message: "Proceed?", initialValue: true }));
|
|
9485
9515
|
if (!proceed) {
|
|
9486
9516
|
me("Aborted.");
|
|
@@ -9634,9 +9664,15 @@ ${detail}`
|
|
|
9634
9664
|
});
|
|
9635
9665
|
Se(lines.join("\n"), "Secrets in other envs");
|
|
9636
9666
|
}
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9667
|
+
let doneMsg;
|
|
9668
|
+
if (failed.length > 0) {
|
|
9669
|
+
doneMsg = `Done with ${failed.length} failure(s) \u2014 see warnings above.`;
|
|
9670
|
+
} else if (scriptWrapSelection.length > 0) {
|
|
9671
|
+
doneMsg = "Done. Your scripts are wrapped \u2014 just run them as usual (e.g. `npm run dev`). Secrets are injected automatically; you never type `keynv exec`.";
|
|
9672
|
+
} else {
|
|
9673
|
+
doneMsg = "Done. Prefix commands with `keynv exec --` to inject secrets (e.g. `keynv exec -- npm run dev`).";
|
|
9674
|
+
}
|
|
9675
|
+
ye(doneMsg);
|
|
9640
9676
|
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
9641
9677
|
}
|
|
9642
9678
|
async function pickOrCreateProject(client, suggestedName) {
|
|
@@ -26864,7 +26900,7 @@ var require_named_placeholders = __commonJS({
|
|
|
26864
26900
|
}
|
|
26865
26901
|
return s;
|
|
26866
26902
|
}
|
|
26867
|
-
function
|
|
26903
|
+
function join18(tree) {
|
|
26868
26904
|
if (tree.length === 1) {
|
|
26869
26905
|
return tree;
|
|
26870
26906
|
}
|
|
@@ -26890,7 +26926,7 @@ var require_named_placeholders = __commonJS({
|
|
|
26890
26926
|
if (cache2 && (tree = cache2.get(query))) {
|
|
26891
26927
|
return toArrayParams(tree, paramsObj);
|
|
26892
26928
|
}
|
|
26893
|
-
tree =
|
|
26929
|
+
tree = join18(parse(query));
|
|
26894
26930
|
if (cache2) {
|
|
26895
26931
|
cache2.set(query, tree);
|
|
26896
26932
|
}
|
|
@@ -30503,7 +30539,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
30503
30539
|
var nodeCrypto = __require("crypto");
|
|
30504
30540
|
module.exports = {
|
|
30505
30541
|
postgresMd5PasswordHash,
|
|
30506
|
-
randomBytes
|
|
30542
|
+
randomBytes,
|
|
30507
30543
|
deriveKey,
|
|
30508
30544
|
sha256,
|
|
30509
30545
|
hashByName,
|
|
@@ -30513,7 +30549,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
30513
30549
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
30514
30550
|
var subtleCrypto = webCrypto.subtle;
|
|
30515
30551
|
var textEncoder = new TextEncoder();
|
|
30516
|
-
function
|
|
30552
|
+
function randomBytes(length) {
|
|
30517
30553
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
30518
30554
|
}
|
|
30519
30555
|
async function md5(string) {
|
|
@@ -44730,17 +44766,20 @@ async function pickConnectionTarget() {
|
|
|
44730
44766
|
const target = await Ee({
|
|
44731
44767
|
message: "Connect to keynv",
|
|
44732
44768
|
options: [
|
|
44733
|
-
{ value: "cloud", label: "keynv.dev", hint: "hosted
|
|
44769
|
+
{ value: "cloud", label: "keynv.dev", hint: "hosted \u2014 fastest way to start" },
|
|
44734
44770
|
{
|
|
44735
44771
|
value: "self-hosted",
|
|
44736
44772
|
label: "Self-hosted server",
|
|
44737
|
-
hint: "
|
|
44773
|
+
hint: "your own deployment \u2014 enter its API URL"
|
|
44738
44774
|
},
|
|
44739
44775
|
{ value: "exit", label: "Exit" }
|
|
44740
44776
|
]
|
|
44741
44777
|
});
|
|
44742
44778
|
if (q(target) || target === "exit") return null;
|
|
44743
|
-
if (target === "cloud")
|
|
44779
|
+
if (target === "cloud") {
|
|
44780
|
+
R2.info(`New to keynv? Create a free account first: ${DEFAULT_WEB_URL}/register`);
|
|
44781
|
+
return DEFAULT_SERVER_URL;
|
|
44782
|
+
}
|
|
44744
44783
|
const server = await Re({
|
|
44745
44784
|
message: "Server API URL",
|
|
44746
44785
|
placeholder: "https://api.keynv.example.com",
|
|
@@ -49795,12 +49834,15 @@ var SecretCreateCommand = class extends Command {
|
|
|
49795
49834
|
var SecretGetCommand = class extends Command {
|
|
49796
49835
|
static paths = [["secret", "get"]];
|
|
49797
49836
|
static usage = Command.Usage({
|
|
49798
|
-
description: "Resolve a secret.
|
|
49837
|
+
description: "Resolve a secret. By default the value is copied to the OS clipboard (never printed). Use --reveal to print it to stdout, or --json for automation."
|
|
49799
49838
|
});
|
|
49800
49839
|
alias = options_exports.String({ required: false });
|
|
49801
49840
|
json = options_exports.Boolean("--json", false);
|
|
49802
49841
|
copy = options_exports.Boolean("--copy", false, {
|
|
49803
|
-
description: "Copy the value to the OS clipboard
|
|
49842
|
+
description: "Copy the value to the OS clipboard (the default in an interactive terminal)."
|
|
49843
|
+
});
|
|
49844
|
+
reveal = options_exports.Boolean("--reveal", false, {
|
|
49845
|
+
description: "Print the raw value to stdout. Opt-in, because it can land in terminal scrollback or an AI transcript."
|
|
49804
49846
|
});
|
|
49805
49847
|
async execute() {
|
|
49806
49848
|
try {
|
|
@@ -49828,35 +49870,41 @@ var SecretGetCommand = class extends Command {
|
|
|
49828
49870
|
const data = await client.request(
|
|
49829
49871
|
`/v1/projects/${projectId3}/secrets/${parsed.environment}/${parsed.key}`
|
|
49830
49872
|
);
|
|
49831
|
-
if (this.copy) {
|
|
49832
|
-
try {
|
|
49833
|
-
await copyToClipboard2(data.value);
|
|
49834
|
-
} catch (err) {
|
|
49835
|
-
if (err instanceof ClipboardUnavailableError) {
|
|
49836
|
-
this.context.stderr.write(
|
|
49837
|
-
`keynv: ${err.message}.
|
|
49838
|
-
Install one (pbcopy/clip are built in; Linux: wl-clipboard or xclip), or omit --copy to print to stdout.
|
|
49839
|
-
`
|
|
49840
|
-
);
|
|
49841
|
-
return 1;
|
|
49842
|
-
}
|
|
49843
|
-
throw err;
|
|
49844
|
-
} finally {
|
|
49845
|
-
data.value = "";
|
|
49846
|
-
}
|
|
49847
|
-
this.context.stderr.write(`keynv: copied ${data.alias} (v${data.version}) to clipboard.
|
|
49848
|
-
`);
|
|
49849
|
-
return 0;
|
|
49850
|
-
}
|
|
49851
49873
|
if (this.json) {
|
|
49852
49874
|
this.context.stdout.write(
|
|
49853
49875
|
`${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
|
|
49854
49876
|
`
|
|
49855
49877
|
);
|
|
49856
|
-
|
|
49878
|
+
return 0;
|
|
49879
|
+
}
|
|
49880
|
+
if (this.reveal) {
|
|
49857
49881
|
this.context.stdout.write(`${data.value}
|
|
49858
49882
|
`);
|
|
49883
|
+
return 0;
|
|
49884
|
+
}
|
|
49885
|
+
if (!this.copy && !isInteractive()) {
|
|
49886
|
+
this.context.stderr.write(
|
|
49887
|
+
"keynv: refusing to print a secret to a non-interactive stdout.\n Pass --reveal to print it, --copy to copy it, or --json for automation.\n"
|
|
49888
|
+
);
|
|
49889
|
+
return 1;
|
|
49859
49890
|
}
|
|
49891
|
+
try {
|
|
49892
|
+
await copyToClipboard2(data.value);
|
|
49893
|
+
} catch (err) {
|
|
49894
|
+
if (err instanceof ClipboardUnavailableError) {
|
|
49895
|
+
this.context.stderr.write(
|
|
49896
|
+
`keynv: ${err.message}.
|
|
49897
|
+
Install one (pbcopy/clip are built in; Linux: wl-clipboard or xclip), or pass --reveal to print to stdout / --json for automation.
|
|
49898
|
+
`
|
|
49899
|
+
);
|
|
49900
|
+
return 1;
|
|
49901
|
+
}
|
|
49902
|
+
throw err;
|
|
49903
|
+
} finally {
|
|
49904
|
+
data.value = "";
|
|
49905
|
+
}
|
|
49906
|
+
this.context.stderr.write(`keynv: copied ${data.alias} (v${data.version}) to clipboard.
|
|
49907
|
+
`);
|
|
49860
49908
|
return 0;
|
|
49861
49909
|
} catch (err) {
|
|
49862
49910
|
return handleExecError(this.context.stderr, err);
|
|
@@ -50126,140 +50174,6 @@ var SecretRotationsCommand = class extends Command {
|
|
|
50126
50174
|
}
|
|
50127
50175
|
}
|
|
50128
50176
|
};
|
|
50129
|
-
function generateSecret(bytes = 32) {
|
|
50130
|
-
return randomBytes(bytes).toString("base64");
|
|
50131
|
-
}
|
|
50132
|
-
var ServerInitCommand = class extends Command {
|
|
50133
|
-
static paths = [["server", "init"]];
|
|
50134
|
-
static usage = Command.Usage({
|
|
50135
|
-
description: "Generate secrets and deployment files for a new keynv server.",
|
|
50136
|
-
details: `
|
|
50137
|
-
Interactive onboarding wizard that generates the configuration
|
|
50138
|
-
a fresh keynv server needs: JWT signing secret, master encryption
|
|
50139
|
-
key, and deployment templates.
|
|
50140
|
-
|
|
50141
|
-
Outputs:
|
|
50142
|
-
\u2022 A .keynv-server.env file you feed into your deployment
|
|
50143
|
-
(Docker Compose, Coolify, or bare-metal).
|
|
50144
|
-
\u2022 Optionally a docker-compose.yml in the current directory.
|
|
50145
|
-
|
|
50146
|
-
Non-interactive with --yes to skip prompts.
|
|
50147
|
-
`,
|
|
50148
|
-
examples: [
|
|
50149
|
-
["Interactive wizard", "$0 server init"],
|
|
50150
|
-
["Non-interactive (CI/scripts)", "$0 server init --yes"],
|
|
50151
|
-
["Docker Compose output", "$0 server init --compose"],
|
|
50152
|
-
["Custom output path", "$0 server init --out ./config/env"]
|
|
50153
|
-
]
|
|
50154
|
-
});
|
|
50155
|
-
yes = options_exports.Boolean("--yes", false, {
|
|
50156
|
-
description: "Skip all prompts (non-interactive)."
|
|
50157
|
-
});
|
|
50158
|
-
compose = options_exports.Boolean("--compose", false, {
|
|
50159
|
-
description: "Also write a docker-compose.yml to the current directory."
|
|
50160
|
-
});
|
|
50161
|
-
out = options_exports.String("--out", {
|
|
50162
|
-
description: "Write server env file to this path (default: .keynv-server.env)."
|
|
50163
|
-
});
|
|
50164
|
-
async execute() {
|
|
50165
|
-
const jwtSecret = generateSecret(48);
|
|
50166
|
-
const masterKey = generateSecret(32);
|
|
50167
|
-
const webSessionSecret = generateSecret(48);
|
|
50168
|
-
const outPath = this.out ?? ".keynv-server.env";
|
|
50169
|
-
const contents = [
|
|
50170
|
-
"# Generated by `keynv server init`.",
|
|
50171
|
-
"# Keep this file secret \u2014 it contains cryptographic key material.",
|
|
50172
|
-
`KEYNV_JWT_SECRET=${jwtSecret}`,
|
|
50173
|
-
`KEYNV_MASTER_KEY=${masterKey}`,
|
|
50174
|
-
"",
|
|
50175
|
-
"# Web dashboard session secret (required for Next.js JWE session tokens).",
|
|
50176
|
-
`KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
|
|
50177
|
-
"",
|
|
50178
|
-
"# Server configuration (uncomment and adjust):",
|
|
50179
|
-
"# KEYNV_PORT=8080",
|
|
50180
|
-
"# KEYNV_WEB_URL=https://keynv.example.com",
|
|
50181
|
-
"# KEYNV_PUBLIC_REGISTRATION=false",
|
|
50182
|
-
"# KEYNV_DB_PATH=./keynv.db",
|
|
50183
|
-
"# KEYNV_MASTER_KEY_FILE=./master.key",
|
|
50184
|
-
"# KEYNV_RATE_LIMIT_PER_MINUTE=120",
|
|
50185
|
-
"# KEYNV_ARGON2_MEMORY_KIB=46080",
|
|
50186
|
-
"# KEYNV_ARGON2_TIME_COST=3"
|
|
50187
|
-
].join("\n");
|
|
50188
|
-
const { writeFileSync: writeFileSync5 } = await import('fs');
|
|
50189
|
-
writeFileSync5(outPath, `${contents}
|
|
50190
|
-
`, { mode: 384 });
|
|
50191
|
-
this.context.stdout.write(`Wrote ${outPath}
|
|
50192
|
-
|
|
50193
|
-
`);
|
|
50194
|
-
this.context.stdout.write("Generated secrets:\n");
|
|
50195
|
-
this.context.stdout.write(
|
|
50196
|
-
` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
50197
|
-
`
|
|
50198
|
-
);
|
|
50199
|
-
this.context.stdout.write(
|
|
50200
|
-
` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
|
|
50201
|
-
`
|
|
50202
|
-
);
|
|
50203
|
-
this.context.stdout.write(
|
|
50204
|
-
` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
50205
|
-
`
|
|
50206
|
-
);
|
|
50207
|
-
this.context.stdout.write("\nNext steps:\n");
|
|
50208
|
-
this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
|
|
50209
|
-
`);
|
|
50210
|
-
this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
|
|
50211
|
-
this.context.stdout.write(" or load it into Coolify as environment variables.\n");
|
|
50212
|
-
this.context.stdout.write(" 2. Bootstrap the first owner:\n");
|
|
50213
|
-
this.context.stdout.write(
|
|
50214
|
-
" docker exec -it <server-container> node dist/bootstrap.js \\\n"
|
|
50215
|
-
);
|
|
50216
|
-
this.context.stdout.write(" --owner-email alice@example.com \\\n");
|
|
50217
|
-
this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
|
|
50218
|
-
this.context.stdout.write(' --org-name "Acme Inc"\n');
|
|
50219
|
-
this.context.stdout.write(" 3. Install the CLI and connect from the TUI:\n");
|
|
50220
|
-
this.context.stdout.write(" npm install -g @keynv/cli\n");
|
|
50221
|
-
this.context.stdout.write(" keynv\n");
|
|
50222
|
-
this.context.stdout.write(" 4. Onboard your first project from the same TUI:\n");
|
|
50223
|
-
this.context.stdout.write(" cd your-project && keynv\n");
|
|
50224
|
-
if (this.compose) {
|
|
50225
|
-
const composeContent = `# keynv server + Litestream sidecar
|
|
50226
|
-
# Generated by \`keynv server init\`.
|
|
50227
|
-
# Load with: docker compose --env-file .keynv-server.env up -d
|
|
50228
|
-
|
|
50229
|
-
services:
|
|
50230
|
-
server:
|
|
50231
|
-
image: ghcr.io/keynv-labs/keynv-server:latest
|
|
50232
|
-
restart: unless-stopped
|
|
50233
|
-
ports:
|
|
50234
|
-
- '8080:8080'
|
|
50235
|
-
env_file:
|
|
50236
|
-
- .keynv-server.env
|
|
50237
|
-
volumes:
|
|
50238
|
-
- keynv-data:/data
|
|
50239
|
-
|
|
50240
|
-
litestream:
|
|
50241
|
-
image: litestream/litestream:latest
|
|
50242
|
-
restart: unless-stopped
|
|
50243
|
-
volumes:
|
|
50244
|
-
- keynv-data:/data
|
|
50245
|
-
command: replicate
|
|
50246
|
-
environment:
|
|
50247
|
-
LITESTREAM_DB_PATH: /data/keynv.db
|
|
50248
|
-
LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
|
|
50249
|
-
AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
|
|
50250
|
-
AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
|
|
50251
|
-
|
|
50252
|
-
volumes:
|
|
50253
|
-
keynv-data:
|
|
50254
|
-
`;
|
|
50255
|
-
writeFileSync5("docker-compose.yml", composeContent, { mode: 384 });
|
|
50256
|
-
this.context.stdout.write(
|
|
50257
|
-
"\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
|
|
50258
|
-
);
|
|
50259
|
-
}
|
|
50260
|
-
return 0;
|
|
50261
|
-
}
|
|
50262
|
-
};
|
|
50263
50177
|
|
|
50264
50178
|
// src/shell/templates.ts
|
|
50265
50179
|
var SHELL_SECRET_ERE = "(AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|gh[uops]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{40,}|xox[abp]-[A-Za-z0-9-]{24,}|(sk|rk)_(live|test)_[A-Za-z0-9]{24,}|sk-ant-(api03|admin01)-[A-Za-z0-9_-]{20,}|sk-(proj-)?[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}|(postgresql|postgres|mysql|mongodb)(\\+srv)?://[^@ ]+@[^ ]+|rediss?://[^@ ]+@[^ ]+";
|
|
@@ -51329,6 +51243,25 @@ the error message.
|
|
|
51329
51243
|
return result.ok ? 0 : 1;
|
|
51330
51244
|
}
|
|
51331
51245
|
};
|
|
51246
|
+
|
|
51247
|
+
// src/commands/ui.ts
|
|
51248
|
+
init_tty();
|
|
51249
|
+
init_menu();
|
|
51250
|
+
var UICommand = class extends Command {
|
|
51251
|
+
static paths = [["ui"]];
|
|
51252
|
+
static usage = Command.Usage({
|
|
51253
|
+
description: "Open the interactive menu (also runs by default when no args are given)."
|
|
51254
|
+
});
|
|
51255
|
+
async execute() {
|
|
51256
|
+
if (!isInteractive()) {
|
|
51257
|
+
this.context.stdout.write(
|
|
51258
|
+
"keynv \u2014 AI-safe secrets management.\nRun `keynv --help` for the full command list, or run `keynv` in an interactive\nterminal to open the menu.\n"
|
|
51259
|
+
);
|
|
51260
|
+
return 0;
|
|
51261
|
+
}
|
|
51262
|
+
return runMenu();
|
|
51263
|
+
}
|
|
51264
|
+
};
|
|
51332
51265
|
init_format();
|
|
51333
51266
|
init_state();
|
|
51334
51267
|
init_version();
|
|
@@ -51837,7 +51770,7 @@ cli.register(WatchStartCommand);
|
|
|
51837
51770
|
cli.register(WatchStopCommand);
|
|
51838
51771
|
cli.register(WatchStatusCommand);
|
|
51839
51772
|
cli.register(TestCommand);
|
|
51840
|
-
cli.register(
|
|
51773
|
+
cli.register(UICommand);
|
|
51841
51774
|
var argv = process.argv.slice(2);
|
|
51842
51775
|
async function main() {
|
|
51843
51776
|
if (argv.length === 0) {
|