@keynv/cli 0.1.0-rc.10 → 0.1.0-rc.12
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/index.js +1050 -605
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
-
import 'crypto';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
4
|
import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { isAbsolute, resolve, join, dirname, basename } from 'path';
|
|
5
|
+
import { hostname, platform, homedir } from 'os';
|
|
6
|
+
import { relative, isAbsolute, resolve, join, dirname, basename } from 'path';
|
|
7
7
|
import { Entry } from '@napi-rs/keyring';
|
|
8
8
|
import { styleText } from 'util';
|
|
9
9
|
import j2, { stdin, stdout } from 'process';
|
|
@@ -579,16 +579,16 @@ var require_lib = __commonJS({
|
|
|
579
579
|
}
|
|
580
580
|
var TypeAssertionError = class extends Error {
|
|
581
581
|
constructor({ errors } = {}) {
|
|
582
|
-
let
|
|
582
|
+
let errorMessage2 = `Type mismatch`;
|
|
583
583
|
if (errors && errors.length > 0) {
|
|
584
|
-
|
|
584
|
+
errorMessage2 += `
|
|
585
585
|
`;
|
|
586
586
|
for (const error of errors) {
|
|
587
|
-
|
|
587
|
+
errorMessage2 += `
|
|
588
588
|
- ${error}`;
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
|
-
super(
|
|
591
|
+
super(errorMessage2);
|
|
592
592
|
}
|
|
593
593
|
};
|
|
594
594
|
function assert(val, validator) {
|
|
@@ -1056,7 +1056,7 @@ var require_lib = __commonJS({
|
|
|
1056
1056
|
var VERSION, AGENT;
|
|
1057
1057
|
var init_version = __esm({
|
|
1058
1058
|
"src/version.ts"() {
|
|
1059
|
-
VERSION = "0.1.0-rc.
|
|
1059
|
+
VERSION = "0.1.0-rc.12" ;
|
|
1060
1060
|
AGENT = `keynv-cli/${VERSION}`;
|
|
1061
1061
|
}
|
|
1062
1062
|
});
|
|
@@ -1146,10 +1146,10 @@ var init_parser = __esm({
|
|
|
1146
1146
|
"../../packages/core/dist/reference/parser.js"() {
|
|
1147
1147
|
PROJECT_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
1148
1148
|
ENV_RE = /^[a-z0-9][a-z0-9-]{0,23}$/;
|
|
1149
|
-
KEY_RE = /^[a-
|
|
1149
|
+
KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
1150
1150
|
TRAILING_BOUNDARY = "(?!\\w|\\.[a-z0-9])";
|
|
1151
|
-
TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-
|
|
1152
|
-
ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-
|
|
1151
|
+
TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
|
|
1152
|
+
ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
|
|
1153
1153
|
TEXT_FIND_RE = new RegExp(TEXT_FIND_SOURCE, "g");
|
|
1154
1154
|
ARGV_FIND_RE = new RegExp(ARGV_FIND_SOURCE, "g");
|
|
1155
1155
|
}
|
|
@@ -1695,15 +1695,15 @@ var init_parseUtil = __esm({
|
|
|
1695
1695
|
message: issueData.message
|
|
1696
1696
|
};
|
|
1697
1697
|
}
|
|
1698
|
-
let
|
|
1698
|
+
let errorMessage2 = "";
|
|
1699
1699
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
1700
1700
|
for (const map of maps) {
|
|
1701
|
-
|
|
1701
|
+
errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
|
|
1702
1702
|
}
|
|
1703
1703
|
return {
|
|
1704
1704
|
...issueData,
|
|
1705
1705
|
path: fullPath,
|
|
1706
|
-
message:
|
|
1706
|
+
message: errorMessage2
|
|
1707
1707
|
};
|
|
1708
1708
|
};
|
|
1709
1709
|
EMPTY_PATH = [];
|
|
@@ -5536,7 +5536,15 @@ function clearCredentialsFile() {
|
|
|
5536
5536
|
if (existsSync(legacy)) rmSync(legacy, { force: true });
|
|
5537
5537
|
try {
|
|
5538
5538
|
entry().deletePassword();
|
|
5539
|
-
|
|
5539
|
+
return true;
|
|
5540
|
+
} catch (err) {
|
|
5541
|
+
process.stderr.write(
|
|
5542
|
+
`keynv: warning \u2014 could not remove OS keychain entry (${err instanceof Error ? err.message : String(err)}).
|
|
5543
|
+
You may need to remove the '${SERVICE}' / '${KEY_ACCOUNT}' entry manually
|
|
5544
|
+
via your OS credential manager (Windows Credential Manager, macOS Keychain, or libsecret).
|
|
5545
|
+
`
|
|
5546
|
+
);
|
|
5547
|
+
return false;
|
|
5540
5548
|
}
|
|
5541
5549
|
}
|
|
5542
5550
|
var SERVICE, KEY_ACCOUNT;
|
|
@@ -5579,8 +5587,9 @@ async function saveCredentials(creds) {
|
|
|
5579
5587
|
cache = creds;
|
|
5580
5588
|
}
|
|
5581
5589
|
function clearCredentials() {
|
|
5582
|
-
clearCredentialsFile();
|
|
5590
|
+
const ok = clearCredentialsFile();
|
|
5583
5591
|
cache = null;
|
|
5592
|
+
return ok;
|
|
5584
5593
|
}
|
|
5585
5594
|
var cache;
|
|
5586
5595
|
var init_store = __esm({
|
|
@@ -5604,6 +5613,7 @@ function buildUrl(base, path, query) {
|
|
|
5604
5613
|
return url.toString();
|
|
5605
5614
|
}
|
|
5606
5615
|
async function tryRefresh(creds) {
|
|
5616
|
+
if (creds.auth_kind === "cli_token" || creds.refresh_token.length === 0) return null;
|
|
5607
5617
|
const res = await fetch(buildUrl(creds.server_url, "/v1/auth/refresh"), {
|
|
5608
5618
|
method: "POST",
|
|
5609
5619
|
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
@@ -5722,6 +5732,21 @@ var init_format = __esm({
|
|
|
5722
5732
|
"src/ui/format.ts"() {
|
|
5723
5733
|
}
|
|
5724
5734
|
});
|
|
5735
|
+
function walkUp(startDir, predicate) {
|
|
5736
|
+
let dir = resolve(startDir);
|
|
5737
|
+
for (let i = 0; i < 64; i++) {
|
|
5738
|
+
const result = predicate(dir);
|
|
5739
|
+
if (result !== null && result !== void 0) return result;
|
|
5740
|
+
const parent = dirname(dir);
|
|
5741
|
+
if (parent === dir) return null;
|
|
5742
|
+
dir = parent;
|
|
5743
|
+
}
|
|
5744
|
+
return null;
|
|
5745
|
+
}
|
|
5746
|
+
var init_fs = __esm({
|
|
5747
|
+
"src/util/fs.ts"() {
|
|
5748
|
+
}
|
|
5749
|
+
});
|
|
5725
5750
|
function parseEnvFile(content, filename) {
|
|
5726
5751
|
const normalized = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
5727
5752
|
const lines = normalized.split(/\r?\n/);
|
|
@@ -5748,7 +5773,7 @@ function parseEnvFile(content, filename) {
|
|
|
5748
5773
|
`invalid key '${name}' (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
|
|
5749
5774
|
);
|
|
5750
5775
|
}
|
|
5751
|
-
|
|
5776
|
+
const valueRaw = body.slice(eq + 1);
|
|
5752
5777
|
let value;
|
|
5753
5778
|
const valueLeading = valueRaw.replace(/^\s+/, "");
|
|
5754
5779
|
const firstCh = valueLeading.charAt(0);
|
|
@@ -5780,20 +5805,14 @@ function parseEnvFile(content, filename) {
|
|
|
5780
5805
|
return entries;
|
|
5781
5806
|
}
|
|
5782
5807
|
function findEnvFile(startDir) {
|
|
5783
|
-
|
|
5784
|
-
for (let i = 0; i < 64; i++) {
|
|
5808
|
+
return walkUp(startDir, (dir) => {
|
|
5785
5809
|
const candidate = join(dir, ENV_FILE_BASENAME);
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
} catch {
|
|
5790
|
-
}
|
|
5810
|
+
try {
|
|
5811
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
5812
|
+
} catch {
|
|
5791
5813
|
}
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
dir = parent;
|
|
5795
|
-
}
|
|
5796
|
-
return null;
|
|
5814
|
+
return null;
|
|
5815
|
+
});
|
|
5797
5816
|
}
|
|
5798
5817
|
function loadEnvFile(opts) {
|
|
5799
5818
|
if (opts.disabled) return null;
|
|
@@ -5818,6 +5837,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
|
|
|
5818
5837
|
var init_envFile = __esm({
|
|
5819
5838
|
"src/exec/envFile.ts"() {
|
|
5820
5839
|
init_dist();
|
|
5840
|
+
init_fs();
|
|
5821
5841
|
MAX_FILE_BYTES = 1e6;
|
|
5822
5842
|
KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5823
5843
|
ENV_FILE_BASENAME = ".keynv.env";
|
|
@@ -5854,16 +5874,129 @@ var init_envFile = __esm({
|
|
|
5854
5874
|
}
|
|
5855
5875
|
});
|
|
5856
5876
|
|
|
5857
|
-
// src/
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5877
|
+
// src/init/heuristics.ts
|
|
5878
|
+
function shannonEntropyBits(s) {
|
|
5879
|
+
if (s.length === 0) return 0;
|
|
5880
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5881
|
+
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
5882
|
+
let h2 = 0;
|
|
5883
|
+
for (const c2 of counts.values()) {
|
|
5884
|
+
const p2 = c2 / s.length;
|
|
5885
|
+
h2 -= p2 * Math.log2(p2);
|
|
5886
|
+
}
|
|
5887
|
+
return h2;
|
|
5864
5888
|
}
|
|
5865
|
-
|
|
5866
|
-
|
|
5889
|
+
function nameMatchesLiteralPrefix(name) {
|
|
5890
|
+
const upper = name.toUpperCase();
|
|
5891
|
+
return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
|
|
5892
|
+
}
|
|
5893
|
+
function nameMatchesSecretSuffix(name) {
|
|
5894
|
+
const upper = name.toUpperCase();
|
|
5895
|
+
for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
|
|
5896
|
+
if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
|
|
5897
|
+
return { matched: true, hint };
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
return { matched: false, hint: "" };
|
|
5901
|
+
}
|
|
5902
|
+
function classifyEntry(name, value) {
|
|
5903
|
+
const upper = name.toUpperCase();
|
|
5904
|
+
if (NAME_FRAMEWORK_MANAGED.has(upper)) {
|
|
5905
|
+
return { verdict: "skip", hint: "framework/shell-managed" };
|
|
5906
|
+
}
|
|
5907
|
+
if (NAME_LITERAL_EXACT.has(upper)) {
|
|
5908
|
+
return { verdict: "literal", hint: "common config var" };
|
|
5909
|
+
}
|
|
5910
|
+
if (nameMatchesLiteralPrefix(name)) {
|
|
5911
|
+
return { verdict: "literal", hint: "public env, build-time bundled" };
|
|
5912
|
+
}
|
|
5913
|
+
if (value.length === 0) {
|
|
5914
|
+
return { verdict: "literal", hint: "empty" };
|
|
5915
|
+
}
|
|
5916
|
+
for (const { re, hint } of VALUE_PATTERNS) {
|
|
5917
|
+
if (re.test(value)) return { verdict: "secret", hint };
|
|
5918
|
+
}
|
|
5919
|
+
const suffix = nameMatchesSecretSuffix(name);
|
|
5920
|
+
if (suffix.matched) {
|
|
5921
|
+
return { verdict: "secret", hint: suffix.hint };
|
|
5922
|
+
}
|
|
5923
|
+
if (NAME_DB_URL.test(name)) {
|
|
5924
|
+
return { verdict: "secret", hint: "database URL" };
|
|
5925
|
+
}
|
|
5926
|
+
if (value.length >= 32) {
|
|
5927
|
+
const bits = shannonEntropyBits(value);
|
|
5928
|
+
if (bits >= 3.5) {
|
|
5929
|
+
return { verdict: "secret", hint: `${value.length}-char random-looking string` };
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
return { verdict: "ambiguous", hint: "" };
|
|
5933
|
+
}
|
|
5934
|
+
function previewValue(value, max = 40) {
|
|
5935
|
+
if (value.length === 0) return "(empty)";
|
|
5936
|
+
if (value.length <= max) return value;
|
|
5937
|
+
return `${value.slice(0, max - 1)}\u2026`;
|
|
5938
|
+
}
|
|
5939
|
+
var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
|
|
5940
|
+
var init_heuristics = __esm({
|
|
5941
|
+
"src/init/heuristics.ts"() {
|
|
5942
|
+
NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
|
|
5943
|
+
"NODE_ENV",
|
|
5944
|
+
"PORT",
|
|
5945
|
+
"HOST",
|
|
5946
|
+
"HOSTNAME",
|
|
5947
|
+
"PATH",
|
|
5948
|
+
"HOME",
|
|
5949
|
+
"USER",
|
|
5950
|
+
"SHELL",
|
|
5951
|
+
"PWD",
|
|
5952
|
+
"CI"
|
|
5953
|
+
]);
|
|
5954
|
+
NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
|
|
5955
|
+
"DEBUG",
|
|
5956
|
+
"LOG_LEVEL",
|
|
5957
|
+
"LOGLEVEL",
|
|
5958
|
+
"TZ",
|
|
5959
|
+
"LANG",
|
|
5960
|
+
"LC_ALL",
|
|
5961
|
+
"NODE_OPTIONS",
|
|
5962
|
+
"NPM_CONFIG_LOGLEVEL",
|
|
5963
|
+
"TS_NODE_PROJECT"
|
|
5964
|
+
]);
|
|
5965
|
+
NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
|
|
5966
|
+
NAME_SECRET_SUFFIXES = [
|
|
5967
|
+
{ suffix: "PRIVATE_KEY", hint: "private key" },
|
|
5968
|
+
{ suffix: "API_KEY", hint: "API key" },
|
|
5969
|
+
{ suffix: "ACCESS_KEY", hint: "access key" },
|
|
5970
|
+
{ suffix: "SECRET_KEY", hint: "secret key" },
|
|
5971
|
+
{ suffix: "SECRET", hint: "secret" },
|
|
5972
|
+
{ suffix: "PASSWORD", hint: "password" },
|
|
5973
|
+
{ suffix: "PASSPHRASE", hint: "passphrase" },
|
|
5974
|
+
{ suffix: "TOKEN", hint: "token" },
|
|
5975
|
+
{ suffix: "KEY", hint: "key" },
|
|
5976
|
+
{ suffix: "CREDENTIALS", hint: "credentials" },
|
|
5977
|
+
{ suffix: "AUTH", hint: "auth" },
|
|
5978
|
+
{ suffix: "DSN", hint: "connection string" }
|
|
5979
|
+
];
|
|
5980
|
+
NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
|
|
5981
|
+
VALUE_PATTERNS = [
|
|
5982
|
+
{ re: /^sk-proj-/, hint: "OpenAI project key" },
|
|
5983
|
+
{ re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
|
|
5984
|
+
{ re: /^sk_live_/, hint: "Stripe live key" },
|
|
5985
|
+
{ re: /^sk_test_/, hint: "Stripe test key" },
|
|
5986
|
+
{ re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
|
|
5987
|
+
{ re: /^xoxb-/, hint: "Slack bot token" },
|
|
5988
|
+
{ re: /^xoxp-/, hint: "Slack user token" },
|
|
5989
|
+
{ re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
|
|
5990
|
+
{ re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
|
|
5991
|
+
{ re: /^gho_/, hint: "GitHub OAuth token" },
|
|
5992
|
+
{ re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
|
|
5993
|
+
{ re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
|
|
5994
|
+
{ re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
|
|
5995
|
+
{ re: /^ya29\./, hint: "Google OAuth access token" },
|
|
5996
|
+
{ re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
|
|
5997
|
+
{ re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
|
|
5998
|
+
{ re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
|
|
5999
|
+
];
|
|
5867
6000
|
}
|
|
5868
6001
|
});
|
|
5869
6002
|
|
|
@@ -7021,23 +7154,109 @@ ${c2}
|
|
|
7021
7154
|
} }).prompt();
|
|
7022
7155
|
}
|
|
7023
7156
|
});
|
|
7157
|
+
function aiContextBody() {
|
|
7158
|
+
return `## keynv (secrets)
|
|
7159
|
+
|
|
7160
|
+
This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
|
|
7161
|
+
|
|
7162
|
+
### Mental model
|
|
7163
|
+
|
|
7164
|
+
- **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
|
|
7165
|
+
- **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
|
|
7166
|
+
- **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
|
|
7167
|
+
- **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
|
|
7168
|
+
|
|
7169
|
+
### What you should do
|
|
7170
|
+
|
|
7171
|
+
When the user asks about secrets or env vars, follow this decision tree:
|
|
7172
|
+
|
|
7173
|
+
| User intent | Run / suggest |
|
|
7174
|
+
|---|---|
|
|
7175
|
+
| "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
|
|
7176
|
+
| "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
|
|
7177
|
+
| "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
|
|
7178
|
+
| "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
|
|
7179
|
+
| "Rotate this key" | \`keynv secret rotate @alias\` |
|
|
7180
|
+
| "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\`) |
|
|
7181
|
+
| "Who has access?" | \`keynv member list <project>\` |
|
|
7182
|
+
|
|
7183
|
+
### Hard rules \u2014 do not violate
|
|
7184
|
+
|
|
7185
|
+
1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
|
|
7186
|
+
2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
|
|
7187
|
+
3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
|
|
7188
|
+
4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
|
|
7189
|
+
5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
|
|
7190
|
+
6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
|
|
7191
|
+
|
|
7192
|
+
### Quick reference
|
|
7193
|
+
|
|
7194
|
+
\`\`\`bash
|
|
7195
|
+
keynv # interactive TUI menu (pick projects, secrets, members)
|
|
7196
|
+
keynv exec -- <cmd> # run cmd with .keynv.env loaded
|
|
7197
|
+
keynv secret create # walk through creating a new secret
|
|
7198
|
+
keynv secret list <project> # list aliases (no values)
|
|
7199
|
+
keynv whoami # who am I logged in as
|
|
7200
|
+
keynv --help # full command list
|
|
7201
|
+
\`\`\`
|
|
7202
|
+
|
|
7203
|
+
If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
|
|
7204
|
+
}
|
|
7205
|
+
function renderKeynvBlock() {
|
|
7206
|
+
return `${KEYNV_BLOCK_START}
|
|
7207
|
+
${aiContextBody()}
|
|
7208
|
+
${KEYNV_BLOCK_END}`;
|
|
7209
|
+
}
|
|
7210
|
+
function writeAiContext(rootPath) {
|
|
7211
|
+
const path = join(rootPath, AGENTS_FILE_BASENAME);
|
|
7212
|
+
const block = renderKeynvBlock();
|
|
7213
|
+
if (!existsSync(path)) {
|
|
7214
|
+
const initial = `# Agent guidance for this project
|
|
7215
|
+
|
|
7216
|
+
${block}
|
|
7217
|
+
`;
|
|
7218
|
+
writeFileSync(path, initial);
|
|
7219
|
+
return "created";
|
|
7220
|
+
}
|
|
7221
|
+
const existing = readFileSync(path, "utf8");
|
|
7222
|
+
const startIdx = existing.indexOf(KEYNV_BLOCK_START);
|
|
7223
|
+
const endIdx = existing.indexOf(KEYNV_BLOCK_END);
|
|
7224
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
7225
|
+
const before = existing.slice(0, startIdx);
|
|
7226
|
+
const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
|
|
7227
|
+
const next = `${before}${block}${after}`;
|
|
7228
|
+
if (next === existing) return "unchanged";
|
|
7229
|
+
writeFileSync(path, next);
|
|
7230
|
+
return "updated";
|
|
7231
|
+
}
|
|
7232
|
+
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
7233
|
+
const trailingNewline = existing.endsWith("\n") ? "" : "\n";
|
|
7234
|
+
writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
|
|
7235
|
+
`);
|
|
7236
|
+
return "appended";
|
|
7237
|
+
}
|
|
7238
|
+
var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
|
|
7239
|
+
var init_aiContext = __esm({
|
|
7240
|
+
"src/init/aiContext.ts"() {
|
|
7241
|
+
AGENTS_FILE_BASENAME = "AGENTS.md";
|
|
7242
|
+
KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
|
|
7243
|
+
KEYNV_BLOCK_END = "<!-- keynv:end -->";
|
|
7244
|
+
}
|
|
7245
|
+
});
|
|
7024
7246
|
function findProjectRoot(startDir) {
|
|
7025
|
-
|
|
7026
|
-
for (let i = 0; i < 64; i++) {
|
|
7247
|
+
const result = walkUp(startDir, (dir) => {
|
|
7027
7248
|
for (const marker of PROJECT_MARKERS) {
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
return buildRoot(dir, marker);
|
|
7249
|
+
if (existsSync(join(dir, marker))) {
|
|
7250
|
+
return { dir, marker };
|
|
7031
7251
|
}
|
|
7032
7252
|
}
|
|
7033
7253
|
if (existsSync(join(dir, GIT_MARKER))) {
|
|
7034
|
-
return
|
|
7254
|
+
return { dir, marker: GIT_MARKER };
|
|
7035
7255
|
}
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
return null;
|
|
7256
|
+
return null;
|
|
7257
|
+
});
|
|
7258
|
+
if (!result) return null;
|
|
7259
|
+
return buildRoot(result.dir, result.marker);
|
|
7041
7260
|
}
|
|
7042
7261
|
function buildRoot(dir, marker) {
|
|
7043
7262
|
let suggestedName = basename(dir);
|
|
@@ -7051,16 +7270,20 @@ function buildRoot(dir, marker) {
|
|
|
7051
7270
|
}
|
|
7052
7271
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
7053
7272
|
scripts = Object.fromEntries(
|
|
7054
|
-
Object.entries(pkg.scripts).filter(
|
|
7055
|
-
([, v2]) => typeof v2 === "string"
|
|
7056
|
-
)
|
|
7273
|
+
Object.entries(pkg.scripts).filter(([, v2]) => typeof v2 === "string")
|
|
7057
7274
|
);
|
|
7058
7275
|
}
|
|
7059
7276
|
} catch {
|
|
7060
7277
|
invalid = true;
|
|
7061
7278
|
}
|
|
7062
7279
|
}
|
|
7063
|
-
return {
|
|
7280
|
+
return {
|
|
7281
|
+
path: dir,
|
|
7282
|
+
suggestedName,
|
|
7283
|
+
marker,
|
|
7284
|
+
packageJsonScripts: scripts,
|
|
7285
|
+
packageJsonInvalid: invalid
|
|
7286
|
+
};
|
|
7064
7287
|
}
|
|
7065
7288
|
function findEnvFiles(rootDir) {
|
|
7066
7289
|
let entries;
|
|
@@ -7118,6 +7341,7 @@ function hasExistingKeynvEnv(rootDir) {
|
|
|
7118
7341
|
var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
|
|
7119
7342
|
var init_detect = __esm({
|
|
7120
7343
|
"src/init/detect.ts"() {
|
|
7344
|
+
init_fs();
|
|
7121
7345
|
PROJECT_MARKERS = [
|
|
7122
7346
|
"package.json",
|
|
7123
7347
|
"pyproject.toml",
|
|
@@ -7134,221 +7358,6 @@ var init_detect = __esm({
|
|
|
7134
7358
|
KEYNV_ENV_BASENAME = ".keynv.env";
|
|
7135
7359
|
}
|
|
7136
7360
|
});
|
|
7137
|
-
function aiContextBody() {
|
|
7138
|
-
return `## keynv (secrets)
|
|
7139
|
-
|
|
7140
|
-
This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
|
|
7141
|
-
|
|
7142
|
-
### Mental model
|
|
7143
|
-
|
|
7144
|
-
- **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
|
|
7145
|
-
- **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
|
|
7146
|
-
- **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
|
|
7147
|
-
- **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
|
|
7148
|
-
|
|
7149
|
-
### What you should do
|
|
7150
|
-
|
|
7151
|
-
When the user asks about secrets or env vars, follow this decision tree:
|
|
7152
|
-
|
|
7153
|
-
| User intent | Run / suggest |
|
|
7154
|
-
|---|---|
|
|
7155
|
-
| "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
|
|
7156
|
-
| "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
|
|
7157
|
-
| "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
|
|
7158
|
-
| "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
|
|
7159
|
-
| "Rotate this key" | \`keynv secret rotate @alias\` |
|
|
7160
|
-
| "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\`) |
|
|
7161
|
-
| "Who has access?" | \`keynv member list <project>\` |
|
|
7162
|
-
|
|
7163
|
-
### Hard rules \u2014 do not violate
|
|
7164
|
-
|
|
7165
|
-
1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
|
|
7166
|
-
2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
|
|
7167
|
-
3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
|
|
7168
|
-
4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
|
|
7169
|
-
5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
|
|
7170
|
-
6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
|
|
7171
|
-
|
|
7172
|
-
### Quick reference
|
|
7173
|
-
|
|
7174
|
-
\`\`\`bash
|
|
7175
|
-
keynv # interactive TUI menu (pick projects, secrets, members)
|
|
7176
|
-
keynv exec -- <cmd> # run cmd with .keynv.env loaded
|
|
7177
|
-
keynv secret create # walk through creating a new secret
|
|
7178
|
-
keynv secret list <project> # list aliases (no values)
|
|
7179
|
-
keynv whoami # who am I logged in as
|
|
7180
|
-
keynv --help # full command list
|
|
7181
|
-
\`\`\`
|
|
7182
|
-
|
|
7183
|
-
If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
|
|
7184
|
-
}
|
|
7185
|
-
function renderKeynvBlock() {
|
|
7186
|
-
return `${KEYNV_BLOCK_START}
|
|
7187
|
-
${aiContextBody()}
|
|
7188
|
-
${KEYNV_BLOCK_END}`;
|
|
7189
|
-
}
|
|
7190
|
-
function writeAiContext(rootPath) {
|
|
7191
|
-
const path = join(rootPath, AGENTS_FILE_BASENAME);
|
|
7192
|
-
const block = renderKeynvBlock();
|
|
7193
|
-
if (!existsSync(path)) {
|
|
7194
|
-
const initial = `# Agent guidance for this project
|
|
7195
|
-
|
|
7196
|
-
${block}
|
|
7197
|
-
`;
|
|
7198
|
-
writeFileSync(path, initial);
|
|
7199
|
-
return "created";
|
|
7200
|
-
}
|
|
7201
|
-
const existing = readFileSync(path, "utf8");
|
|
7202
|
-
const startIdx = existing.indexOf(KEYNV_BLOCK_START);
|
|
7203
|
-
const endIdx = existing.indexOf(KEYNV_BLOCK_END);
|
|
7204
|
-
if (startIdx >= 0 && endIdx > startIdx) {
|
|
7205
|
-
const before = existing.slice(0, startIdx);
|
|
7206
|
-
const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
|
|
7207
|
-
const next = `${before}${block}${after}`;
|
|
7208
|
-
if (next === existing) return "unchanged";
|
|
7209
|
-
writeFileSync(path, next);
|
|
7210
|
-
return "updated";
|
|
7211
|
-
}
|
|
7212
|
-
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
7213
|
-
const trailingNewline = existing.endsWith("\n") ? "" : "\n";
|
|
7214
|
-
writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
|
|
7215
|
-
`);
|
|
7216
|
-
return "appended";
|
|
7217
|
-
}
|
|
7218
|
-
var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
|
|
7219
|
-
var init_aiContext = __esm({
|
|
7220
|
-
"src/init/aiContext.ts"() {
|
|
7221
|
-
AGENTS_FILE_BASENAME = "AGENTS.md";
|
|
7222
|
-
KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
|
|
7223
|
-
KEYNV_BLOCK_END = "<!-- keynv:end -->";
|
|
7224
|
-
}
|
|
7225
|
-
});
|
|
7226
|
-
|
|
7227
|
-
// src/init/heuristics.ts
|
|
7228
|
-
function shannonEntropyBits(s) {
|
|
7229
|
-
if (s.length === 0) return 0;
|
|
7230
|
-
const counts = /* @__PURE__ */ new Map();
|
|
7231
|
-
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
7232
|
-
let h2 = 0;
|
|
7233
|
-
for (const c2 of counts.values()) {
|
|
7234
|
-
const p2 = c2 / s.length;
|
|
7235
|
-
h2 -= p2 * Math.log2(p2);
|
|
7236
|
-
}
|
|
7237
|
-
return h2;
|
|
7238
|
-
}
|
|
7239
|
-
function nameMatchesLiteralPrefix(name) {
|
|
7240
|
-
const upper = name.toUpperCase();
|
|
7241
|
-
return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
|
|
7242
|
-
}
|
|
7243
|
-
function nameMatchesSecretSuffix(name) {
|
|
7244
|
-
const upper = name.toUpperCase();
|
|
7245
|
-
for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
|
|
7246
|
-
if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
|
|
7247
|
-
return { matched: true, hint };
|
|
7248
|
-
}
|
|
7249
|
-
}
|
|
7250
|
-
return { matched: false, hint: "" };
|
|
7251
|
-
}
|
|
7252
|
-
function classifyEntry(name, value) {
|
|
7253
|
-
const upper = name.toUpperCase();
|
|
7254
|
-
if (NAME_FRAMEWORK_MANAGED.has(upper)) {
|
|
7255
|
-
return { verdict: "skip", hint: "framework/shell-managed" };
|
|
7256
|
-
}
|
|
7257
|
-
if (NAME_LITERAL_EXACT.has(upper)) {
|
|
7258
|
-
return { verdict: "literal", hint: "common config var" };
|
|
7259
|
-
}
|
|
7260
|
-
if (nameMatchesLiteralPrefix(name)) {
|
|
7261
|
-
return { verdict: "literal", hint: "public env, build-time bundled" };
|
|
7262
|
-
}
|
|
7263
|
-
if (value.length === 0) {
|
|
7264
|
-
return { verdict: "literal", hint: "empty" };
|
|
7265
|
-
}
|
|
7266
|
-
for (const { re, hint } of VALUE_PATTERNS) {
|
|
7267
|
-
if (re.test(value)) return { verdict: "secret", hint };
|
|
7268
|
-
}
|
|
7269
|
-
const suffix = nameMatchesSecretSuffix(name);
|
|
7270
|
-
if (suffix.matched) {
|
|
7271
|
-
return { verdict: "secret", hint: suffix.hint };
|
|
7272
|
-
}
|
|
7273
|
-
if (NAME_DB_URL.test(name)) {
|
|
7274
|
-
return { verdict: "secret", hint: "database URL" };
|
|
7275
|
-
}
|
|
7276
|
-
if (value.length >= 32) {
|
|
7277
|
-
const bits = shannonEntropyBits(value);
|
|
7278
|
-
if (bits >= 3.5) {
|
|
7279
|
-
return { verdict: "secret", hint: `${value.length}-char random-looking string` };
|
|
7280
|
-
}
|
|
7281
|
-
}
|
|
7282
|
-
return { verdict: "ambiguous", hint: "" };
|
|
7283
|
-
}
|
|
7284
|
-
function previewValue(value, max = 40) {
|
|
7285
|
-
if (value.length === 0) return "(empty)";
|
|
7286
|
-
if (value.length <= max) return value;
|
|
7287
|
-
return `${value.slice(0, max - 1)}\u2026`;
|
|
7288
|
-
}
|
|
7289
|
-
var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
|
|
7290
|
-
var init_heuristics = __esm({
|
|
7291
|
-
"src/init/heuristics.ts"() {
|
|
7292
|
-
NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
|
|
7293
|
-
"NODE_ENV",
|
|
7294
|
-
"PORT",
|
|
7295
|
-
"HOST",
|
|
7296
|
-
"HOSTNAME",
|
|
7297
|
-
"PATH",
|
|
7298
|
-
"HOME",
|
|
7299
|
-
"USER",
|
|
7300
|
-
"SHELL",
|
|
7301
|
-
"PWD",
|
|
7302
|
-
"CI"
|
|
7303
|
-
]);
|
|
7304
|
-
NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
|
|
7305
|
-
"DEBUG",
|
|
7306
|
-
"LOG_LEVEL",
|
|
7307
|
-
"LOGLEVEL",
|
|
7308
|
-
"TZ",
|
|
7309
|
-
"LANG",
|
|
7310
|
-
"LC_ALL",
|
|
7311
|
-
"NODE_OPTIONS",
|
|
7312
|
-
"NPM_CONFIG_LOGLEVEL",
|
|
7313
|
-
"TS_NODE_PROJECT"
|
|
7314
|
-
]);
|
|
7315
|
-
NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
|
|
7316
|
-
NAME_SECRET_SUFFIXES = [
|
|
7317
|
-
{ suffix: "PRIVATE_KEY", hint: "private key" },
|
|
7318
|
-
{ suffix: "API_KEY", hint: "API key" },
|
|
7319
|
-
{ suffix: "ACCESS_KEY", hint: "access key" },
|
|
7320
|
-
{ suffix: "SECRET_KEY", hint: "secret key" },
|
|
7321
|
-
{ suffix: "SECRET", hint: "secret" },
|
|
7322
|
-
{ suffix: "PASSWORD", hint: "password" },
|
|
7323
|
-
{ suffix: "PASSPHRASE", hint: "passphrase" },
|
|
7324
|
-
{ suffix: "TOKEN", hint: "token" },
|
|
7325
|
-
{ suffix: "KEY", hint: "key" },
|
|
7326
|
-
{ suffix: "CREDENTIALS", hint: "credentials" },
|
|
7327
|
-
{ suffix: "AUTH", hint: "auth" },
|
|
7328
|
-
{ suffix: "DSN", hint: "connection string" }
|
|
7329
|
-
];
|
|
7330
|
-
NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
|
|
7331
|
-
VALUE_PATTERNS = [
|
|
7332
|
-
{ re: /^sk-proj-/, hint: "OpenAI project key" },
|
|
7333
|
-
{ re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
|
|
7334
|
-
{ re: /^sk_live_/, hint: "Stripe live key" },
|
|
7335
|
-
{ re: /^sk_test_/, hint: "Stripe test key" },
|
|
7336
|
-
{ re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
|
|
7337
|
-
{ re: /^xoxb-/, hint: "Slack bot token" },
|
|
7338
|
-
{ re: /^xoxp-/, hint: "Slack user token" },
|
|
7339
|
-
{ re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
|
|
7340
|
-
{ re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
|
|
7341
|
-
{ re: /^gho_/, hint: "GitHub OAuth token" },
|
|
7342
|
-
{ re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
|
|
7343
|
-
{ re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
|
|
7344
|
-
{ re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
|
|
7345
|
-
{ re: /^ya29\./, hint: "Google OAuth access token" },
|
|
7346
|
-
{ re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
|
|
7347
|
-
{ re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
|
|
7348
|
-
{ re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
|
|
7349
|
-
];
|
|
7350
|
-
}
|
|
7351
|
-
});
|
|
7352
7361
|
|
|
7353
7362
|
// src/init/scriptWrap.ts
|
|
7354
7363
|
function analyzeScript(name, command) {
|
|
@@ -7641,7 +7650,9 @@ ${detail}`
|
|
|
7641
7650
|
}
|
|
7642
7651
|
const totalEntries = [...perEnv.values()].reduce((n, arr) => n + arr.length, 0);
|
|
7643
7652
|
if (totalEntries === 0) {
|
|
7644
|
-
R2.info(
|
|
7653
|
+
R2.info(
|
|
7654
|
+
"All env files were empty or only contained framework-managed vars. Nothing to upload."
|
|
7655
|
+
);
|
|
7645
7656
|
ye("Done.");
|
|
7646
7657
|
return { exitCode: 0 };
|
|
7647
7658
|
}
|
|
@@ -7693,10 +7704,10 @@ ${detail}`
|
|
|
7693
7704
|
const planSummary = [
|
|
7694
7705
|
`Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
|
|
7695
7706
|
`Environments: ${distinctEnvs.join(", ")}`,
|
|
7696
|
-
|
|
7707
|
+
"Per-env breakdown:",
|
|
7697
7708
|
perEnvCounts,
|
|
7698
7709
|
`Script wraps: ${scriptWrapSelection.length}`,
|
|
7699
|
-
|
|
7710
|
+
"Original .env: delete after upload",
|
|
7700
7711
|
opts.dryRun ? "Dry-run: no changes will be made." : ""
|
|
7701
7712
|
].filter(Boolean).join("\n");
|
|
7702
7713
|
Se(planSummary, "About to apply");
|
|
@@ -7731,7 +7742,11 @@ ${detail}`
|
|
|
7731
7742
|
method: "POST",
|
|
7732
7743
|
body: { env: env2, key: aliasKey, value: e2.value }
|
|
7733
7744
|
});
|
|
7734
|
-
const alias2 = buildAlias({
|
|
7745
|
+
const alias2 = buildAlias({
|
|
7746
|
+
project: projectChoice.name,
|
|
7747
|
+
environment: env2,
|
|
7748
|
+
key: aliasKey
|
|
7749
|
+
});
|
|
7735
7750
|
if (alias2 === null) {
|
|
7736
7751
|
failed.push({
|
|
7737
7752
|
env: env2,
|
|
@@ -7753,7 +7768,9 @@ ${detail}`
|
|
|
7753
7768
|
if (failed.length === 0) {
|
|
7754
7769
|
s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
|
|
7755
7770
|
} else {
|
|
7756
|
-
s.error(
|
|
7771
|
+
s.error(
|
|
7772
|
+
`${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`
|
|
7773
|
+
);
|
|
7757
7774
|
for (const f of failed) R2.warn(` [${f.env}] ${f.name}: ${f.reason}`);
|
|
7758
7775
|
}
|
|
7759
7776
|
}
|
|
@@ -7968,7 +7985,9 @@ async function ensureProjectAndEnvs(client, projectChoice, distinctEnvs) {
|
|
|
7968
7985
|
body: envBodyFor(envName)
|
|
7969
7986
|
});
|
|
7970
7987
|
} catch (err) {
|
|
7971
|
-
s.error(
|
|
7988
|
+
s.error(
|
|
7989
|
+
`Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`
|
|
7990
|
+
);
|
|
7972
7991
|
return null;
|
|
7973
7992
|
}
|
|
7974
7993
|
}
|
|
@@ -7987,7 +8006,8 @@ function composeKeynvEnv(opts) {
|
|
|
7987
8006
|
const { uploadedAliases, literals, mergeWithExisting } = opts;
|
|
7988
8007
|
const lines = [];
|
|
7989
8008
|
if (mergeWithExisting !== null) {
|
|
7990
|
-
const
|
|
8009
|
+
const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
8010
|
+
const trimmed = normalized.replace(/\n+$/, "");
|
|
7991
8011
|
if (trimmed.length > 0) {
|
|
7992
8012
|
lines.push(...trimmed.split("\n"));
|
|
7993
8013
|
lines.push("");
|
|
@@ -8014,7 +8034,7 @@ function composeKeynvEnv(opts) {
|
|
|
8014
8034
|
}
|
|
8015
8035
|
}
|
|
8016
8036
|
if (mergeWithExisting !== null) {
|
|
8017
|
-
lines.push(
|
|
8037
|
+
lines.push("# <<< keynv init <<<");
|
|
8018
8038
|
}
|
|
8019
8039
|
return lines;
|
|
8020
8040
|
}
|
|
@@ -8035,11 +8055,11 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
|
|
|
8035
8055
|
}
|
|
8036
8056
|
var init_init = __esm({
|
|
8037
8057
|
"src/ui/flows/init.ts"() {
|
|
8038
|
-
init_dist();
|
|
8039
8058
|
init_dist5();
|
|
8059
|
+
init_dist();
|
|
8040
8060
|
init_envFile();
|
|
8041
|
-
init_detect();
|
|
8042
8061
|
init_aiContext();
|
|
8062
|
+
init_detect();
|
|
8043
8063
|
init_heuristics();
|
|
8044
8064
|
init_scriptWrap();
|
|
8045
8065
|
init_cancel();
|
|
@@ -8047,30 +8067,122 @@ var init_init = __esm({
|
|
|
8047
8067
|
}
|
|
8048
8068
|
});
|
|
8049
8069
|
|
|
8050
|
-
// src/ui/helpers/
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8070
|
+
// src/ui/helpers/tty.ts
|
|
8071
|
+
var tty_exports = {};
|
|
8072
|
+
__export(tty_exports, {
|
|
8073
|
+
isInteractive: () => isInteractive
|
|
8074
|
+
});
|
|
8075
|
+
function isInteractive() {
|
|
8076
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
8056
8077
|
}
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8078
|
+
var init_tty = __esm({
|
|
8079
|
+
"src/ui/helpers/tty.ts"() {
|
|
8080
|
+
}
|
|
8081
|
+
});
|
|
8082
|
+
function sleep(ms) {
|
|
8083
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
8084
|
+
}
|
|
8085
|
+
async function errorMessage(res, fallback) {
|
|
8086
|
+
const text = await res.text();
|
|
8087
|
+
if (!text) return fallback;
|
|
8088
|
+
try {
|
|
8089
|
+
const parsed = JSON.parse(text);
|
|
8090
|
+
return parsed.error?.message ?? fallback;
|
|
8091
|
+
} catch {
|
|
8092
|
+
return fallback;
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
function openBrowser(url) {
|
|
8096
|
+
try {
|
|
8097
|
+
const os = platform();
|
|
8098
|
+
const command = os === "win32" ? "cmd" : os === "darwin" ? "open" : "xdg-open";
|
|
8099
|
+
const args = os === "win32" ? ["/c", "start", "", url] : [url];
|
|
8100
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
8101
|
+
child.unref();
|
|
8102
|
+
return true;
|
|
8103
|
+
} catch {
|
|
8104
|
+
return false;
|
|
8105
|
+
}
|
|
8106
|
+
}
|
|
8107
|
+
async function runBrowserAuth(serverUrl) {
|
|
8108
|
+
const startRes = await fetch(new URL("/v1/auth/cli/browser/start", serverUrl).toString(), {
|
|
8109
|
+
method: "POST",
|
|
8110
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
8111
|
+
body: JSON.stringify({ device_name: hostname() })
|
|
8067
8112
|
});
|
|
8068
|
-
if (
|
|
8069
|
-
|
|
8113
|
+
if (!startRes.ok) {
|
|
8114
|
+
throw new BrowserAuthError(
|
|
8115
|
+
await errorMessage(startRes, `Browser auth failed to start (${startRes.status}).`)
|
|
8116
|
+
);
|
|
8117
|
+
}
|
|
8118
|
+
const start = await startRes.json();
|
|
8119
|
+
const opened = openBrowser(start.verification_uri_complete);
|
|
8120
|
+
if (opened) {
|
|
8121
|
+
process.stderr.write(
|
|
8122
|
+
`
|
|
8123
|
+
Your code: ${start.user_code}
|
|
8124
|
+
Complete auth in your browser, then return here.
|
|
8125
|
+
|
|
8126
|
+
`
|
|
8127
|
+
);
|
|
8128
|
+
} else {
|
|
8129
|
+
process.stderr.write(
|
|
8130
|
+
`
|
|
8131
|
+
Could not open a browser automatically.
|
|
8132
|
+
Open this URL manually:
|
|
8133
|
+
|
|
8134
|
+
${start.verification_uri_complete}
|
|
8135
|
+
|
|
8136
|
+
Your code: ${start.user_code}
|
|
8137
|
+
Waiting for you to complete auth in the browser...
|
|
8138
|
+
|
|
8139
|
+
`
|
|
8140
|
+
);
|
|
8141
|
+
}
|
|
8142
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
8143
|
+
const intervalMs = Math.max(1, start.interval) * 1e3;
|
|
8144
|
+
while (Date.now() < deadline) {
|
|
8145
|
+
await sleep(intervalMs);
|
|
8146
|
+
const pollRes = await fetch(new URL("/v1/auth/cli/browser/poll", serverUrl).toString(), {
|
|
8147
|
+
method: "POST",
|
|
8148
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
8149
|
+
body: JSON.stringify({ device_code: start.device_code })
|
|
8150
|
+
});
|
|
8151
|
+
if (pollRes.status === 202) continue;
|
|
8152
|
+
if (!pollRes.ok) {
|
|
8153
|
+
throw new BrowserAuthError(
|
|
8154
|
+
await errorMessage(pollRes, `Browser auth failed (${pollRes.status}).`)
|
|
8155
|
+
);
|
|
8156
|
+
}
|
|
8157
|
+
const data = await pollRes.json();
|
|
8158
|
+
return {
|
|
8159
|
+
auth_kind: "session",
|
|
8160
|
+
server_url: serverUrl,
|
|
8161
|
+
user_id: data.user.id,
|
|
8162
|
+
email: data.user.email,
|
|
8163
|
+
org_id: data.user.org_id,
|
|
8164
|
+
org_role: data.user.org_role,
|
|
8165
|
+
access_token: data.access_token,
|
|
8166
|
+
refresh_token: data.refresh_token,
|
|
8167
|
+
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
8168
|
+
};
|
|
8169
|
+
}
|
|
8170
|
+
throw new BrowserAuthError("Browser auth timed out. Run `keynv login` to try again.");
|
|
8070
8171
|
}
|
|
8071
|
-
var
|
|
8072
|
-
|
|
8073
|
-
|
|
8172
|
+
var BrowserAuthError;
|
|
8173
|
+
var init_browser_auth = __esm({
|
|
8174
|
+
"src/client/browser-auth.ts"() {
|
|
8175
|
+
init_version();
|
|
8176
|
+
BrowserAuthError = class extends Error {
|
|
8177
|
+
};
|
|
8178
|
+
}
|
|
8179
|
+
});
|
|
8180
|
+
|
|
8181
|
+
// src/client/defaults.ts
|
|
8182
|
+
var DEFAULT_SERVER_URL;
|
|
8183
|
+
var init_defaults = __esm({
|
|
8184
|
+
"src/client/defaults.ts"() {
|
|
8185
|
+
DEFAULT_SERVER_URL = "https://api.keynv.dev";
|
|
8074
8186
|
}
|
|
8075
8187
|
});
|
|
8076
8188
|
|
|
@@ -8098,6 +8210,33 @@ var init_pickEnv = __esm({
|
|
|
8098
8210
|
init_dist5();
|
|
8099
8211
|
}
|
|
8100
8212
|
});
|
|
8213
|
+
|
|
8214
|
+
// src/ui/helpers/pickSecret.ts
|
|
8215
|
+
async function listSecrets(client, projectId2) {
|
|
8216
|
+
const data = await client.request(
|
|
8217
|
+
`/v1/projects/${projectId2}/secrets`
|
|
8218
|
+
);
|
|
8219
|
+
return data.secrets;
|
|
8220
|
+
}
|
|
8221
|
+
async function pickSecret(client, projectId2, message = "Secret:") {
|
|
8222
|
+
const secrets = await listSecrets(client, projectId2);
|
|
8223
|
+
if (secrets.length === 0) return null;
|
|
8224
|
+
const value = await Ee({
|
|
8225
|
+
message,
|
|
8226
|
+
options: secrets.map((s) => ({
|
|
8227
|
+
value: s.alias,
|
|
8228
|
+
label: s.alias,
|
|
8229
|
+
hint: `v${s.version}`
|
|
8230
|
+
}))
|
|
8231
|
+
});
|
|
8232
|
+
if (q(value)) return null;
|
|
8233
|
+
return secrets.find((s) => s.alias === value) ?? null;
|
|
8234
|
+
}
|
|
8235
|
+
var init_pickSecret = __esm({
|
|
8236
|
+
"src/ui/helpers/pickSecret.ts"() {
|
|
8237
|
+
init_dist5();
|
|
8238
|
+
}
|
|
8239
|
+
});
|
|
8101
8240
|
async function runSecretsFlow(client, project) {
|
|
8102
8241
|
let target = project;
|
|
8103
8242
|
if (!target) {
|
|
@@ -8161,7 +8300,10 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8161
8300
|
}
|
|
8162
8301
|
} else if (choice === "rotate") {
|
|
8163
8302
|
const newValue = unwrap(
|
|
8164
|
-
await Ce({
|
|
8303
|
+
await Ce({
|
|
8304
|
+
message: "New value",
|
|
8305
|
+
validate: (v2) => v2?.length ? void 0 : "required"
|
|
8306
|
+
})
|
|
8165
8307
|
);
|
|
8166
8308
|
const data = await client.request(`${path}/rotate`, {
|
|
8167
8309
|
method: "POST",
|
|
@@ -8169,9 +8311,7 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8169
8311
|
});
|
|
8170
8312
|
R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
|
|
8171
8313
|
} else if (choice === "delete") {
|
|
8172
|
-
const confirmed = unwrap(
|
|
8173
|
-
await ue({ message: `Delete ${alias2}?`, initialValue: false })
|
|
8174
|
-
);
|
|
8314
|
+
const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
|
|
8175
8315
|
if (!confirmed) continue;
|
|
8176
8316
|
await client.request(path, { method: "DELETE" });
|
|
8177
8317
|
R2.success(`Deleted ${alias2}`);
|
|
@@ -8180,11 +8320,13 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8180
8320
|
}
|
|
8181
8321
|
}
|
|
8182
8322
|
async function copyToClipboard(value) {
|
|
8183
|
-
const
|
|
8184
|
-
const cmd =
|
|
8323
|
+
const platform2 = process.platform;
|
|
8324
|
+
const cmd = platform2 === "darwin" ? ["pbcopy", []] : platform2 === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
|
|
8185
8325
|
return new Promise((resolve3) => {
|
|
8186
8326
|
try {
|
|
8187
|
-
const child = spawn(cmd[0], cmd[1], {
|
|
8327
|
+
const child = spawn(cmd[0], cmd[1], {
|
|
8328
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
8329
|
+
});
|
|
8188
8330
|
child.on("error", () => resolve3(false));
|
|
8189
8331
|
child.on("exit", (code) => resolve3(code === 0));
|
|
8190
8332
|
child.stdin.end(value);
|
|
@@ -8211,7 +8353,7 @@ async function promptNewSecret(client, project) {
|
|
|
8211
8353
|
}),
|
|
8212
8354
|
value: () => Ce({
|
|
8213
8355
|
message: "Value (hidden)",
|
|
8214
|
-
validate: (v2) => v2
|
|
8356
|
+
validate: (v2) => v2?.length ? void 0 : "required"
|
|
8215
8357
|
})
|
|
8216
8358
|
},
|
|
8217
8359
|
{
|
|
@@ -8240,8 +8382,8 @@ async function createSecretInteractive(client, project) {
|
|
|
8240
8382
|
}
|
|
8241
8383
|
var init_secret = __esm({
|
|
8242
8384
|
"src/ui/flows/secret.ts"() {
|
|
8243
|
-
init_dist();
|
|
8244
8385
|
init_dist5();
|
|
8386
|
+
init_dist();
|
|
8245
8387
|
init_cancel();
|
|
8246
8388
|
init_pickEnv();
|
|
8247
8389
|
init_pickProject();
|
|
@@ -28574,7 +28716,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
28574
28716
|
var nodeCrypto = __require("crypto");
|
|
28575
28717
|
module.exports = {
|
|
28576
28718
|
postgresMd5PasswordHash,
|
|
28577
|
-
randomBytes,
|
|
28719
|
+
randomBytes: randomBytes2,
|
|
28578
28720
|
deriveKey,
|
|
28579
28721
|
sha256,
|
|
28580
28722
|
hashByName,
|
|
@@ -28584,7 +28726,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
28584
28726
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
28585
28727
|
var subtleCrypto = webCrypto.subtle;
|
|
28586
28728
|
var textEncoder = new TextEncoder();
|
|
28587
|
-
function
|
|
28729
|
+
function randomBytes2(length) {
|
|
28588
28730
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
28589
28731
|
}
|
|
28590
28732
|
async function md5(string) {
|
|
@@ -28980,11 +29122,11 @@ var require_pg_connection_string = __commonJS({
|
|
|
28980
29122
|
config.client_encoding = result.searchParams.get("encoding");
|
|
28981
29123
|
return config;
|
|
28982
29124
|
}
|
|
28983
|
-
const
|
|
29125
|
+
const hostname2 = dummyHost ? "" : result.hostname;
|
|
28984
29126
|
if (!config.host) {
|
|
28985
|
-
config.host = decodeURIComponent(
|
|
28986
|
-
} else if (
|
|
28987
|
-
result.pathname =
|
|
29127
|
+
config.host = decodeURIComponent(hostname2);
|
|
29128
|
+
} else if (hostname2 && /^%2f/i.test(hostname2)) {
|
|
29129
|
+
result.pathname = hostname2 + result.pathname;
|
|
28988
29130
|
}
|
|
28989
29131
|
if (!config.port) {
|
|
28990
29132
|
config.port = result.port;
|
|
@@ -40090,9 +40232,9 @@ var require_cluster = __commonJS({
|
|
|
40090
40232
|
}
|
|
40091
40233
|
});
|
|
40092
40234
|
}
|
|
40093
|
-
resolveSrv(
|
|
40235
|
+
resolveSrv(hostname2) {
|
|
40094
40236
|
return new Promise((resolve3, reject) => {
|
|
40095
|
-
this.options.resolveSrv(
|
|
40237
|
+
this.options.resolveSrv(hostname2, (err, records) => {
|
|
40096
40238
|
if (err) {
|
|
40097
40239
|
return reject(err);
|
|
40098
40240
|
}
|
|
@@ -40114,14 +40256,14 @@ var require_cluster = __commonJS({
|
|
|
40114
40256
|
});
|
|
40115
40257
|
});
|
|
40116
40258
|
}
|
|
40117
|
-
dnsLookup(
|
|
40259
|
+
dnsLookup(hostname2) {
|
|
40118
40260
|
return new Promise((resolve3, reject) => {
|
|
40119
|
-
this.options.dnsLookup(
|
|
40261
|
+
this.options.dnsLookup(hostname2, (err, address) => {
|
|
40120
40262
|
if (err) {
|
|
40121
|
-
debug2("failed to resolve hostname %s to IP: %s",
|
|
40263
|
+
debug2("failed to resolve hostname %s to IP: %s", hostname2, err.message);
|
|
40122
40264
|
reject(err);
|
|
40123
40265
|
} else {
|
|
40124
|
-
debug2("resolved hostname %s to IP %s",
|
|
40266
|
+
debug2("resolved hostname %s to IP %s", hostname2, address);
|
|
40125
40267
|
resolve3(address);
|
|
40126
40268
|
}
|
|
40127
40269
|
});
|
|
@@ -42446,85 +42588,27 @@ var init_audit2 = __esm({
|
|
|
42446
42588
|
});
|
|
42447
42589
|
|
|
42448
42590
|
// src/ui/flows/login.ts
|
|
42449
|
-
async function runLoginFlow(client) {
|
|
42450
|
-
const
|
|
42451
|
-
{
|
|
42452
|
-
server: () => Re({
|
|
42453
|
-
message: "Server URL",
|
|
42454
|
-
placeholder: "http://localhost:8080",
|
|
42455
|
-
defaultValue: "http://localhost:8080"
|
|
42456
|
-
}),
|
|
42457
|
-
email: () => Re({
|
|
42458
|
-
message: "Email",
|
|
42459
|
-
validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
|
|
42460
|
-
}),
|
|
42461
|
-
password: () => Ce({
|
|
42462
|
-
message: "Password",
|
|
42463
|
-
validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
|
|
42464
|
-
})
|
|
42465
|
-
},
|
|
42466
|
-
{
|
|
42467
|
-
onCancel: () => {
|
|
42468
|
-
throw new UserCancelled();
|
|
42469
|
-
}
|
|
42470
|
-
}
|
|
42471
|
-
);
|
|
42472
|
-
const server = answers.server || "http://localhost:8080";
|
|
42591
|
+
async function runLoginFlow(client, options = {}) {
|
|
42592
|
+
const server = options.server ?? DEFAULT_SERVER_URL;
|
|
42473
42593
|
const s = ft();
|
|
42474
|
-
s.start("
|
|
42475
|
-
let
|
|
42594
|
+
s.start("Waiting for authorization");
|
|
42595
|
+
let creds;
|
|
42476
42596
|
try {
|
|
42477
|
-
|
|
42478
|
-
method: "POST",
|
|
42479
|
-
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
42480
|
-
body: JSON.stringify({ email: answers.email, password: answers.password })
|
|
42481
|
-
});
|
|
42597
|
+
creds = await runBrowserAuth(server);
|
|
42482
42598
|
} catch (err) {
|
|
42483
|
-
s.error("
|
|
42484
|
-
me(err instanceof Error ? err.message : "
|
|
42485
|
-
return false;
|
|
42486
|
-
}
|
|
42487
|
-
if (!res.ok) {
|
|
42488
|
-
let msg = `login failed (${res.status})`;
|
|
42489
|
-
try {
|
|
42490
|
-
const parsed = await res.json();
|
|
42491
|
-
msg = parsed.error?.message ?? msg;
|
|
42492
|
-
} catch {
|
|
42493
|
-
}
|
|
42494
|
-
s.error(msg);
|
|
42599
|
+
s.error("Browser login failed");
|
|
42600
|
+
me(err instanceof Error ? err.message : "Unable to authenticate.");
|
|
42495
42601
|
return false;
|
|
42496
42602
|
}
|
|
42497
|
-
|
|
42498
|
-
|
|
42499
|
-
server_url: server,
|
|
42500
|
-
user_id: data.user.id,
|
|
42501
|
-
email: data.user.email,
|
|
42502
|
-
org_id: data.user.org_id,
|
|
42503
|
-
org_role: data.user.org_role,
|
|
42504
|
-
access_token: data.access_token,
|
|
42505
|
-
refresh_token: data.refresh_token,
|
|
42506
|
-
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
42507
|
-
});
|
|
42508
|
-
await client.setCredentials({
|
|
42509
|
-
server_url: server,
|
|
42510
|
-
user_id: data.user.id,
|
|
42511
|
-
email: data.user.email,
|
|
42512
|
-
org_id: data.user.org_id,
|
|
42513
|
-
org_role: data.user.org_role,
|
|
42514
|
-
access_token: data.access_token,
|
|
42515
|
-
refresh_token: data.refresh_token,
|
|
42516
|
-
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
42517
|
-
});
|
|
42518
|
-
s.stop(`Logged in as ${data.user.email}`);
|
|
42603
|
+
await client.setCredentials(creds);
|
|
42604
|
+
s.stop(`Logged in as ${creds.email}`);
|
|
42519
42605
|
return true;
|
|
42520
42606
|
}
|
|
42521
42607
|
var init_login = __esm({
|
|
42522
42608
|
"src/ui/flows/login.ts"() {
|
|
42523
42609
|
init_dist5();
|
|
42524
|
-
|
|
42525
|
-
|
|
42526
|
-
init_version();
|
|
42527
|
-
init_cancel();
|
|
42610
|
+
init_browser_auth();
|
|
42611
|
+
init_defaults();
|
|
42528
42612
|
}
|
|
42529
42613
|
});
|
|
42530
42614
|
|
|
@@ -42583,7 +42667,7 @@ async function addMemberInteractive(client, project) {
|
|
|
42583
42667
|
{
|
|
42584
42668
|
email: () => Re({
|
|
42585
42669
|
message: "Email",
|
|
42586
|
-
validate: (v2) => v2
|
|
42670
|
+
validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
|
|
42587
42671
|
}),
|
|
42588
42672
|
role: () => Ee({
|
|
42589
42673
|
message: "Role",
|
|
@@ -42682,8 +42766,11 @@ function printDetail(detail) {
|
|
|
42682
42766
|
const envLines = detail.environments.map(
|
|
42683
42767
|
(e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
|
|
42684
42768
|
);
|
|
42685
|
-
Se(
|
|
42686
|
-
|
|
42769
|
+
Se(
|
|
42770
|
+
`${detail.name} (${detail.id})
|
|
42771
|
+
${envLines.join("\n") || " (no environments)"}`,
|
|
42772
|
+
"Project"
|
|
42773
|
+
);
|
|
42687
42774
|
}
|
|
42688
42775
|
async function createProjectInteractive(client) {
|
|
42689
42776
|
const answers = await he(
|
|
@@ -42742,6 +42829,7 @@ async function runMenu() {
|
|
|
42742
42829
|
ge(`keynv ${VERSION}`);
|
|
42743
42830
|
const client = new ApiClient();
|
|
42744
42831
|
await client.ensureHydrated();
|
|
42832
|
+
let didLogin = false;
|
|
42745
42833
|
if (!client.isLoggedIn) {
|
|
42746
42834
|
R2.info("Not logged in.");
|
|
42747
42835
|
try {
|
|
@@ -42750,6 +42838,7 @@ async function runMenu() {
|
|
|
42750
42838
|
ye("Login cancelled.");
|
|
42751
42839
|
return 1;
|
|
42752
42840
|
}
|
|
42841
|
+
didLogin = true;
|
|
42753
42842
|
} catch (err) {
|
|
42754
42843
|
if (err instanceof UserCancelled) {
|
|
42755
42844
|
me("Login cancelled.");
|
|
@@ -42761,6 +42850,20 @@ async function runMenu() {
|
|
|
42761
42850
|
const u = client.currentUser;
|
|
42762
42851
|
if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
|
|
42763
42852
|
}
|
|
42853
|
+
if (didLogin) {
|
|
42854
|
+
const root = findProjectRoot(process.cwd());
|
|
42855
|
+
const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
|
|
42856
|
+
if (!alreadyInitialized) {
|
|
42857
|
+
const setup = await ue({
|
|
42858
|
+
message: "Set up this project now?",
|
|
42859
|
+
initialValue: true
|
|
42860
|
+
});
|
|
42861
|
+
if (!q(setup) && setup) {
|
|
42862
|
+
const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
42863
|
+
await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
|
|
42864
|
+
}
|
|
42865
|
+
}
|
|
42866
|
+
}
|
|
42764
42867
|
while (true) {
|
|
42765
42868
|
let choice;
|
|
42766
42869
|
try {
|
|
@@ -42827,15 +42930,16 @@ var init_menu = __esm({
|
|
|
42827
42930
|
"src/ui/menu.ts"() {
|
|
42828
42931
|
init_dist5();
|
|
42829
42932
|
init_http();
|
|
42933
|
+
init_detect();
|
|
42830
42934
|
init_store();
|
|
42831
|
-
|
|
42832
|
-
init_cancel();
|
|
42935
|
+
init_version();
|
|
42833
42936
|
init_audit2();
|
|
42834
42937
|
init_login();
|
|
42835
42938
|
init_member();
|
|
42836
42939
|
init_project();
|
|
42837
42940
|
init_secret();
|
|
42838
|
-
|
|
42941
|
+
init_format();
|
|
42942
|
+
init_cancel();
|
|
42839
42943
|
}
|
|
42840
42944
|
});
|
|
42841
42945
|
|
|
@@ -43516,11 +43620,11 @@ var reducers = {
|
|
|
43516
43620
|
return { ...state, options: [{ name: `-c`, value: String(command) }] };
|
|
43517
43621
|
}
|
|
43518
43622
|
},
|
|
43519
|
-
setError: (state, segment, segmentIndex,
|
|
43623
|
+
setError: (state, segment, segmentIndex, errorMessage2) => {
|
|
43520
43624
|
if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
|
|
43521
|
-
return { ...state, errorMessage: `${
|
|
43625
|
+
return { ...state, errorMessage: `${errorMessage2}.` };
|
|
43522
43626
|
} else {
|
|
43523
|
-
return { ...state, errorMessage: `${
|
|
43627
|
+
return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
|
|
43524
43628
|
}
|
|
43525
43629
|
},
|
|
43526
43630
|
setOptionArityError: (state, segment) => {
|
|
@@ -44657,11 +44761,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
|
|
|
44657
44761
|
return resolved;
|
|
44658
44762
|
}
|
|
44659
44763
|
function substitute(text, resolved) {
|
|
44764
|
+
const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
|
|
44660
44765
|
let out = text;
|
|
44661
|
-
for (const r of
|
|
44662
|
-
|
|
44663
|
-
out = out.split(r.alias.literal).join(r.value);
|
|
44664
|
-
}
|
|
44766
|
+
for (const r of sorted) {
|
|
44767
|
+
out = out.split(r.alias.literal).join(r.value);
|
|
44665
44768
|
}
|
|
44666
44769
|
return out;
|
|
44667
44770
|
}
|
|
@@ -44764,10 +44867,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
|
|
|
44764
44867
|
|
|
44765
44868
|
// ../../packages/redactor/dist/entropy.js
|
|
44766
44869
|
var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
|
|
44870
|
+
var DEFAULT_EXCLUDE_PREFIXES = [
|
|
44871
|
+
"sha1-",
|
|
44872
|
+
"sha256:",
|
|
44873
|
+
"sha256-",
|
|
44874
|
+
"sha384-",
|
|
44875
|
+
"sha512-"
|
|
44876
|
+
];
|
|
44767
44877
|
var DEFAULTS = {
|
|
44768
44878
|
enabled: true,
|
|
44769
44879
|
minLength: 24,
|
|
44770
|
-
minBitsPerChar: 4.5
|
|
44880
|
+
minBitsPerChar: 4.5,
|
|
44881
|
+
excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
|
|
44771
44882
|
};
|
|
44772
44883
|
function shannonEntropy(s) {
|
|
44773
44884
|
if (s.length === 0)
|
|
@@ -44800,9 +44911,13 @@ function findEntropyMatches(text, opts = {}) {
|
|
|
44800
44911
|
const end = i;
|
|
44801
44912
|
const token = text.slice(start, end);
|
|
44802
44913
|
if (token.length >= cfg.minLength) {
|
|
44803
|
-
const
|
|
44804
|
-
|
|
44805
|
-
|
|
44914
|
+
const lower = token.toLowerCase();
|
|
44915
|
+
const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
|
|
44916
|
+
if (!excluded) {
|
|
44917
|
+
const h2 = shannonEntropy(token);
|
|
44918
|
+
if (h2 >= cfg.minBitsPerChar) {
|
|
44919
|
+
matches.push({ start, end, token });
|
|
44920
|
+
}
|
|
44806
44921
|
}
|
|
44807
44922
|
}
|
|
44808
44923
|
}
|
|
@@ -44946,7 +45061,19 @@ var ENV_ALLOWLIST = [
|
|
|
44946
45061
|
"PWD",
|
|
44947
45062
|
"OLDPWD",
|
|
44948
45063
|
"TMPDIR",
|
|
44949
|
-
"SSH_AUTH_SOCK"
|
|
45064
|
+
"SSH_AUTH_SOCK",
|
|
45065
|
+
// Windows-specific
|
|
45066
|
+
"USERPROFILE",
|
|
45067
|
+
"USERNAME",
|
|
45068
|
+
"COMPUTERNAME",
|
|
45069
|
+
"TEMP",
|
|
45070
|
+
"TMP",
|
|
45071
|
+
"SYSTEMROOT",
|
|
45072
|
+
"SYSTEMDRIVE",
|
|
45073
|
+
"WINDIR",
|
|
45074
|
+
"COMSPEC",
|
|
45075
|
+
"APPDATA",
|
|
45076
|
+
"LOCALAPPDATA"
|
|
44950
45077
|
];
|
|
44951
45078
|
function spawnPrivileged(opts) {
|
|
44952
45079
|
const startedAt = Date.now();
|
|
@@ -44962,7 +45089,8 @@ function spawnPrivileged(opts) {
|
|
|
44962
45089
|
const child = spawn(opts.command, opts.args, {
|
|
44963
45090
|
env: env2,
|
|
44964
45091
|
stdio,
|
|
44965
|
-
detached: false
|
|
45092
|
+
detached: false,
|
|
45093
|
+
shell: process.platform === "win32"
|
|
44966
45094
|
});
|
|
44967
45095
|
const literals = opts.resolved.map((r) => r.value).filter((v2) => v2.length > 0);
|
|
44968
45096
|
if (!opts.noRedact && child.stdout) {
|
|
@@ -45026,10 +45154,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
|
|
|
45026
45154
|
spelled \`--from\` to avoid the collision.)
|
|
45027
45155
|
`,
|
|
45028
45156
|
examples: [
|
|
45029
|
-
[
|
|
45030
|
-
"Auto-load .keynv.env from cwd or parents",
|
|
45031
|
-
"$0 exec -- next dev"
|
|
45032
|
-
],
|
|
45157
|
+
["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
|
|
45033
45158
|
[
|
|
45034
45159
|
"Run mysql with the alias substituted at fork-exec time",
|
|
45035
45160
|
"$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
|
|
@@ -45093,7 +45218,11 @@ spelled \`--from\` to avoid the collision.)
|
|
|
45093
45218
|
`);
|
|
45094
45219
|
return 2;
|
|
45095
45220
|
}
|
|
45096
|
-
|
|
45221
|
+
this.context.stderr.write(
|
|
45222
|
+
`keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
|
|
45223
|
+
`
|
|
45224
|
+
);
|
|
45225
|
+
return 2;
|
|
45097
45226
|
}
|
|
45098
45227
|
const viaEnvSpecs = [];
|
|
45099
45228
|
for (const spec of this.viaEnv ?? []) {
|
|
@@ -45165,11 +45294,18 @@ spelled \`--from\` to avoid the collision.)
|
|
|
45165
45294
|
injectedEnv[spec.name] = value;
|
|
45166
45295
|
}
|
|
45167
45296
|
if (envFileLoaded && !this.quiet) {
|
|
45168
|
-
const
|
|
45169
|
-
const
|
|
45297
|
+
const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
|
|
45298
|
+
const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
|
|
45299
|
+
const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
|
|
45300
|
+
const parts = [];
|
|
45301
|
+
if (aliasEntries.length > 0) {
|
|
45302
|
+
parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
|
|
45303
|
+
}
|
|
45304
|
+
if (plainEntries.length > 0) {
|
|
45305
|
+
parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
|
|
45306
|
+
}
|
|
45170
45307
|
this.context.stderr.write(
|
|
45171
|
-
`keynv: loaded ${
|
|
45172
|
-
`
|
|
45308
|
+
`keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
|
|
45173
45309
|
);
|
|
45174
45310
|
}
|
|
45175
45311
|
const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
|
|
@@ -45209,10 +45345,142 @@ function signalNumber(sig) {
|
|
|
45209
45345
|
}
|
|
45210
45346
|
|
|
45211
45347
|
// src/commands/init.ts
|
|
45348
|
+
init_dist();
|
|
45212
45349
|
init_http();
|
|
45213
|
-
|
|
45350
|
+
init_envFile();
|
|
45351
|
+
init_heuristics();
|
|
45214
45352
|
init_init();
|
|
45215
45353
|
init_cancel();
|
|
45354
|
+
init_tty();
|
|
45355
|
+
|
|
45356
|
+
// src/commands/project.ts
|
|
45357
|
+
init_http();
|
|
45358
|
+
init_format();
|
|
45359
|
+
function isProjectId(input) {
|
|
45360
|
+
return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
|
|
45361
|
+
}
|
|
45362
|
+
async function resolveProjectId(client, input) {
|
|
45363
|
+
if (isProjectId(input)) {
|
|
45364
|
+
return input;
|
|
45365
|
+
}
|
|
45366
|
+
const data = await client.request("/v1/projects");
|
|
45367
|
+
const match = data.projects.find((p2) => p2.name === input);
|
|
45368
|
+
if (!match) throw new Error(`project not found: ${input}`);
|
|
45369
|
+
return match.id;
|
|
45370
|
+
}
|
|
45371
|
+
var ProjectListCommand = class extends Command {
|
|
45372
|
+
static paths = [["project", "list"]];
|
|
45373
|
+
static usage = Command.Usage({
|
|
45374
|
+
description: "List projects visible to the current user."
|
|
45375
|
+
});
|
|
45376
|
+
json = options_exports.Boolean("--json", false);
|
|
45377
|
+
async execute() {
|
|
45378
|
+
const client = new ApiClient();
|
|
45379
|
+
const data = await client.request("/v1/projects");
|
|
45380
|
+
if (this.json) {
|
|
45381
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45382
|
+
`);
|
|
45383
|
+
return 0;
|
|
45384
|
+
}
|
|
45385
|
+
if (data.projects.length === 0) {
|
|
45386
|
+
this.context.stdout.write("no projects\n");
|
|
45387
|
+
return 0;
|
|
45388
|
+
}
|
|
45389
|
+
this.context.stdout.write(
|
|
45390
|
+
`${table(
|
|
45391
|
+
["name", "id", "created_at"],
|
|
45392
|
+
data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
|
|
45393
|
+
)}
|
|
45394
|
+
`
|
|
45395
|
+
);
|
|
45396
|
+
return 0;
|
|
45397
|
+
}
|
|
45398
|
+
};
|
|
45399
|
+
var ProjectCreateCommand = class extends Command {
|
|
45400
|
+
static paths = [["project", "create"]];
|
|
45401
|
+
static usage = Command.Usage({
|
|
45402
|
+
description: "Create a new project with one or more environments.",
|
|
45403
|
+
examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
|
|
45404
|
+
});
|
|
45405
|
+
name = options_exports.String();
|
|
45406
|
+
envs = options_exports.Array("--env", {
|
|
45407
|
+
description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
|
|
45408
|
+
});
|
|
45409
|
+
async execute() {
|
|
45410
|
+
const envs = (this.envs ?? ["dev"]).map((spec) => {
|
|
45411
|
+
const [name, tier, approval] = spec.split(":");
|
|
45412
|
+
return {
|
|
45413
|
+
name: name ?? "",
|
|
45414
|
+
tier: tier ?? "non-production",
|
|
45415
|
+
require_approval: approval === "approval"
|
|
45416
|
+
};
|
|
45417
|
+
});
|
|
45418
|
+
const client = new ApiClient();
|
|
45419
|
+
const result = await client.request("/v1/projects", {
|
|
45420
|
+
method: "POST",
|
|
45421
|
+
body: { name: this.name, environments: envs }
|
|
45422
|
+
});
|
|
45423
|
+
this.context.stdout.write(`created project ${result.name} (${result.id})
|
|
45424
|
+
`);
|
|
45425
|
+
for (const e2 of envs) {
|
|
45426
|
+
this.context.stdout.write(
|
|
45427
|
+
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45428
|
+
`
|
|
45429
|
+
);
|
|
45430
|
+
}
|
|
45431
|
+
return 0;
|
|
45432
|
+
}
|
|
45433
|
+
};
|
|
45434
|
+
var ProjectDescribeCommand = class extends Command {
|
|
45435
|
+
static paths = [["project", "describe"]];
|
|
45436
|
+
static usage = Command.Usage({
|
|
45437
|
+
description: "Show metadata for one project.",
|
|
45438
|
+
examples: [
|
|
45439
|
+
["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
|
|
45440
|
+
["By name", "$0 project describe myproject"]
|
|
45441
|
+
]
|
|
45442
|
+
});
|
|
45443
|
+
id = options_exports.String();
|
|
45444
|
+
json = options_exports.Boolean("--json", false);
|
|
45445
|
+
async execute() {
|
|
45446
|
+
const client = new ApiClient();
|
|
45447
|
+
const projectId2 = await resolveProjectId(client, this.id);
|
|
45448
|
+
const data = await client.request(`/v1/projects/${projectId2}`);
|
|
45449
|
+
if (this.json) {
|
|
45450
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45451
|
+
`);
|
|
45452
|
+
return 0;
|
|
45453
|
+
}
|
|
45454
|
+
this.context.stdout.write(`project: ${data.name} (${data.id})
|
|
45455
|
+
`);
|
|
45456
|
+
for (const e2 of data.environments) {
|
|
45457
|
+
this.context.stdout.write(
|
|
45458
|
+
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45459
|
+
`
|
|
45460
|
+
);
|
|
45461
|
+
}
|
|
45462
|
+
return 0;
|
|
45463
|
+
}
|
|
45464
|
+
};
|
|
45465
|
+
var ProjectDeleteCommand = class extends Command {
|
|
45466
|
+
static paths = [["project", "delete"]];
|
|
45467
|
+
static usage = Command.Usage({ description: "Soft-delete a project." });
|
|
45468
|
+
id = options_exports.String();
|
|
45469
|
+
force = options_exports.Boolean("--force", false);
|
|
45470
|
+
async execute() {
|
|
45471
|
+
if (!this.force) {
|
|
45472
|
+
this.context.stderr.write("keynv: refusing to delete without --force\n");
|
|
45473
|
+
return 2;
|
|
45474
|
+
}
|
|
45475
|
+
const client = new ApiClient();
|
|
45476
|
+
await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
|
|
45477
|
+
this.context.stdout.write(`deleted project ${this.id}
|
|
45478
|
+
`);
|
|
45479
|
+
return 0;
|
|
45480
|
+
}
|
|
45481
|
+
};
|
|
45482
|
+
|
|
45483
|
+
// src/commands/init.ts
|
|
45216
45484
|
var InitCommand = class extends Command {
|
|
45217
45485
|
static paths = [["init"]];
|
|
45218
45486
|
static usage = Command.Usage({
|
|
@@ -45226,6 +45494,11 @@ package.json scripts with \`keynv exec\`.
|
|
|
45226
45494
|
Safe to re-run: existing .keynv.env entries are preserved; new
|
|
45227
45495
|
entries are appended below a marker.
|
|
45228
45496
|
|
|
45497
|
+
--dry-run prints the secrets that would be uploaded and the alias
|
|
45498
|
+
mappings that would be written to .keynv.env, then exits without
|
|
45499
|
+
touching any files or making any network calls. Use it to preview
|
|
45500
|
+
what init will do before committing.
|
|
45501
|
+
|
|
45229
45502
|
Requires an interactive terminal (clack TUI). For scripted
|
|
45230
45503
|
migration, use the lower-level \`keynv project\` and \`keynv secret\`
|
|
45231
45504
|
commands directly.
|
|
@@ -45233,28 +45506,47 @@ commands directly.
|
|
|
45233
45506
|
examples: [
|
|
45234
45507
|
["Walk the current project", "$0 init"],
|
|
45235
45508
|
["Preview without writing or uploading", "$0 init --dry-run"],
|
|
45236
|
-
["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
|
|
45509
|
+
["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
|
|
45510
|
+
["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
|
|
45237
45511
|
]
|
|
45238
45512
|
});
|
|
45239
45513
|
dryRun = options_exports.Boolean("--dry-run", false, {
|
|
45240
|
-
description: "
|
|
45514
|
+
description: "Scan .env files and print the secrets that would be uploaded and the alias mappings that would be written \u2014 no files written, no vault calls made."
|
|
45241
45515
|
});
|
|
45242
45516
|
noScripts = options_exports.Boolean("--no-scripts", false, {
|
|
45243
45517
|
description: "Skip the package.json script-wrapping step."
|
|
45244
45518
|
});
|
|
45519
|
+
envFile = options_exports.String("--env-file", {
|
|
45520
|
+
description: "Path to .env file to migrate (non-interactive mode)."
|
|
45521
|
+
});
|
|
45522
|
+
project = options_exports.String("--project", {
|
|
45523
|
+
description: "Project name or ID (non-interactive mode)."
|
|
45524
|
+
});
|
|
45525
|
+
env = options_exports.String("--env", {
|
|
45526
|
+
description: "Environment name for --env-file (default: dev)."
|
|
45527
|
+
});
|
|
45528
|
+
secret = options_exports.Array("--secret", {
|
|
45529
|
+
description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
|
|
45530
|
+
});
|
|
45245
45531
|
async execute() {
|
|
45246
|
-
if (!isInteractive()) {
|
|
45247
|
-
this.context.stderr.write(
|
|
45248
|
-
"keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
|
|
45249
|
-
);
|
|
45250
|
-
return 1;
|
|
45251
|
-
}
|
|
45252
45532
|
const client = new ApiClient();
|
|
45253
45533
|
await client.ensureHydrated();
|
|
45254
45534
|
if (!client.isLoggedIn) {
|
|
45255
45535
|
this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
|
|
45256
45536
|
return 1;
|
|
45257
45537
|
}
|
|
45538
|
+
const hasEnvFile = this.envFile != null;
|
|
45539
|
+
const hasSecrets = this.secret != null && this.secret.length > 0;
|
|
45540
|
+
const isNonInteractive = hasEnvFile || hasSecrets;
|
|
45541
|
+
if (isNonInteractive) {
|
|
45542
|
+
return this.runNonInteractive(client);
|
|
45543
|
+
}
|
|
45544
|
+
if (!isInteractive()) {
|
|
45545
|
+
this.context.stderr.write(
|
|
45546
|
+
"keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
|
|
45547
|
+
);
|
|
45548
|
+
return 1;
|
|
45549
|
+
}
|
|
45258
45550
|
try {
|
|
45259
45551
|
const outcome = await runInitFlow(client, {
|
|
45260
45552
|
cwd: process.cwd(),
|
|
@@ -45270,9 +45562,102 @@ commands directly.
|
|
|
45270
45562
|
return 1;
|
|
45271
45563
|
}
|
|
45272
45564
|
}
|
|
45565
|
+
async runNonInteractive(client) {
|
|
45566
|
+
const projectName = this.project;
|
|
45567
|
+
if (!projectName) {
|
|
45568
|
+
this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
|
|
45569
|
+
return 1;
|
|
45570
|
+
}
|
|
45571
|
+
const resolved = await resolveProjectId(client, projectName);
|
|
45572
|
+
if (!resolved) {
|
|
45573
|
+
this.context.stderr.write(`keynv: project not found: ${projectName}
|
|
45574
|
+
`);
|
|
45575
|
+
return 1;
|
|
45576
|
+
}
|
|
45577
|
+
const projectId2 = resolved;
|
|
45578
|
+
const envName = this.env ?? "dev";
|
|
45579
|
+
const secrets = [];
|
|
45580
|
+
if (this.envFile) {
|
|
45581
|
+
try {
|
|
45582
|
+
const content = readFileSync(this.envFile, "utf8");
|
|
45583
|
+
const entries = parseEnvFile(content, this.envFile);
|
|
45584
|
+
for (const e2 of entries) {
|
|
45585
|
+
if (classifyEntry(e2.name, e2.value).verdict !== "skip") {
|
|
45586
|
+
secrets.push({ name: e2.name, value: e2.value });
|
|
45587
|
+
}
|
|
45588
|
+
}
|
|
45589
|
+
} catch (err) {
|
|
45590
|
+
this.context.stderr.write(
|
|
45591
|
+
`keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
|
|
45592
|
+
`
|
|
45593
|
+
);
|
|
45594
|
+
return 1;
|
|
45595
|
+
}
|
|
45596
|
+
}
|
|
45597
|
+
if (this.secret) {
|
|
45598
|
+
for (const spec of this.secret) {
|
|
45599
|
+
const eq = spec.indexOf("=");
|
|
45600
|
+
if (eq <= 0) {
|
|
45601
|
+
this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
|
|
45602
|
+
`);
|
|
45603
|
+
return 1;
|
|
45604
|
+
}
|
|
45605
|
+
const name = spec.slice(0, eq);
|
|
45606
|
+
const value = spec.slice(eq + 1);
|
|
45607
|
+
secrets.push({ name, value });
|
|
45608
|
+
}
|
|
45609
|
+
}
|
|
45610
|
+
if (secrets.length === 0) {
|
|
45611
|
+
this.context.stdout.write("keynv: nothing to migrate.\n");
|
|
45612
|
+
return 0;
|
|
45613
|
+
}
|
|
45614
|
+
if (this.dryRun) {
|
|
45615
|
+
this.context.stdout.write(
|
|
45616
|
+
`keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
|
|
45617
|
+
`
|
|
45618
|
+
);
|
|
45619
|
+
for (const { name } of secrets) {
|
|
45620
|
+
const aliasKey = name.toLowerCase().replace(/_/g, "-");
|
|
45621
|
+
this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
|
|
45622
|
+
`);
|
|
45623
|
+
}
|
|
45624
|
+
return 0;
|
|
45625
|
+
}
|
|
45626
|
+
let uploaded = 0;
|
|
45627
|
+
const failed = [];
|
|
45628
|
+
for (const { name, value } of secrets) {
|
|
45629
|
+
const aliasKey = name.toLowerCase().replace(/_/g, "-");
|
|
45630
|
+
const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
|
|
45631
|
+
if (!alias2) {
|
|
45632
|
+
failed.push(`${name} (invalid alias key: ${aliasKey})`);
|
|
45633
|
+
continue;
|
|
45634
|
+
}
|
|
45635
|
+
try {
|
|
45636
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
45637
|
+
method: "POST",
|
|
45638
|
+
body: { env: envName, key: aliasKey, value }
|
|
45639
|
+
});
|
|
45640
|
+
uploaded++;
|
|
45641
|
+
} catch (err) {
|
|
45642
|
+
failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45643
|
+
}
|
|
45644
|
+
}
|
|
45645
|
+
this.context.stdout.write(
|
|
45646
|
+
`keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
|
|
45647
|
+
`
|
|
45648
|
+
);
|
|
45649
|
+
if (failed.length > 0) {
|
|
45650
|
+
for (const f of failed) this.context.stderr.write(` failed: ${f}
|
|
45651
|
+
`);
|
|
45652
|
+
return 1;
|
|
45653
|
+
}
|
|
45654
|
+
return 0;
|
|
45655
|
+
}
|
|
45273
45656
|
};
|
|
45274
45657
|
|
|
45275
45658
|
// src/commands/login.ts
|
|
45659
|
+
init_browser_auth();
|
|
45660
|
+
init_defaults();
|
|
45276
45661
|
init_http();
|
|
45277
45662
|
init_store();
|
|
45278
45663
|
init_format();
|
|
@@ -45333,18 +45718,40 @@ var LoginCommand = class extends Command {
|
|
|
45333
45718
|
static usage = Command.Usage({
|
|
45334
45719
|
description: "Authenticate against a keynv server.",
|
|
45335
45720
|
examples: [
|
|
45336
|
-
["
|
|
45721
|
+
["Browser login", "$0 login"],
|
|
45722
|
+
["Self-hosted browser login", "$0 login --server http://localhost:8080"],
|
|
45723
|
+
["Headless token login", "$0 login --server http://... --token kt_..."],
|
|
45337
45724
|
["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
|
|
45338
45725
|
]
|
|
45339
45726
|
});
|
|
45340
45727
|
server = options_exports.String("--server", { description: "Server base URL." });
|
|
45728
|
+
token = options_exports.String("--token", { description: "CLI token for headless auth." });
|
|
45341
45729
|
email = options_exports.String("--email", { description: "Email address." });
|
|
45342
45730
|
password = options_exports.String("--password", {
|
|
45343
45731
|
description: "Password (use stdin to avoid argv leak)."
|
|
45344
45732
|
});
|
|
45345
45733
|
async execute() {
|
|
45346
|
-
const
|
|
45347
|
-
|
|
45734
|
+
const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
|
|
45735
|
+
if (this.token) {
|
|
45736
|
+
return this.loginWithToken(finalServerUrl, this.token);
|
|
45737
|
+
}
|
|
45738
|
+
if (!this.email && !this.password) {
|
|
45739
|
+
this.context.stdout.write(`Opening browser for ${finalServerUrl} ...
|
|
45740
|
+
`);
|
|
45741
|
+
try {
|
|
45742
|
+
const creds = await runBrowserAuth(finalServerUrl);
|
|
45743
|
+
await saveCredentials(creds);
|
|
45744
|
+
this.context.stdout.write(`logged in as ${creds.email} (${creds.org_role})
|
|
45745
|
+
`);
|
|
45746
|
+
return 0;
|
|
45747
|
+
} catch (err) {
|
|
45748
|
+
this.context.stderr.write(
|
|
45749
|
+
`keynv: ${err instanceof Error ? err.message : "browser login failed"}
|
|
45750
|
+
`
|
|
45751
|
+
);
|
|
45752
|
+
return 1;
|
|
45753
|
+
}
|
|
45754
|
+
}
|
|
45348
45755
|
const email2 = this.email ?? await promptLine("email: ");
|
|
45349
45756
|
const password = this.password ?? await promptHidden("password: ");
|
|
45350
45757
|
const res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
|
|
@@ -45375,6 +45782,7 @@ var LoginCommand = class extends Command {
|
|
|
45375
45782
|
const data = await res.json();
|
|
45376
45783
|
try {
|
|
45377
45784
|
await saveCredentials({
|
|
45785
|
+
auth_kind: "session",
|
|
45378
45786
|
server_url: finalServerUrl,
|
|
45379
45787
|
user_id: data.user.id,
|
|
45380
45788
|
email: data.user.email,
|
|
@@ -45392,6 +45800,54 @@ var LoginCommand = class extends Command {
|
|
|
45392
45800
|
return 1;
|
|
45393
45801
|
}
|
|
45394
45802
|
this.context.stdout.write(`logged in as ${data.user.email} (${data.user.org_role})
|
|
45803
|
+
`);
|
|
45804
|
+
return 0;
|
|
45805
|
+
}
|
|
45806
|
+
async loginWithToken(serverUrl, token) {
|
|
45807
|
+
const res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
|
|
45808
|
+
headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
|
|
45809
|
+
});
|
|
45810
|
+
if (!res.ok) {
|
|
45811
|
+
const text = await res.text();
|
|
45812
|
+
let msg = `token login failed (${res.status})`;
|
|
45813
|
+
try {
|
|
45814
|
+
const parsed = JSON.parse(text);
|
|
45815
|
+
msg = parsed.error?.message ?? msg;
|
|
45816
|
+
this.context.stderr.write(
|
|
45817
|
+
`${fmtError({
|
|
45818
|
+
status: res.status,
|
|
45819
|
+
...parsed.error?.code ? { code: parsed.error.code } : {},
|
|
45820
|
+
message: msg
|
|
45821
|
+
})}
|
|
45822
|
+
`
|
|
45823
|
+
);
|
|
45824
|
+
} catch {
|
|
45825
|
+
this.context.stderr.write(`keynv: ${msg}
|
|
45826
|
+
`);
|
|
45827
|
+
}
|
|
45828
|
+
return 1;
|
|
45829
|
+
}
|
|
45830
|
+
const data = await res.json();
|
|
45831
|
+
try {
|
|
45832
|
+
await saveCredentials({
|
|
45833
|
+
auth_kind: "cli_token",
|
|
45834
|
+
server_url: serverUrl,
|
|
45835
|
+
user_id: data.id,
|
|
45836
|
+
email: data.email,
|
|
45837
|
+
org_id: data.org_id,
|
|
45838
|
+
org_role: data.org_role,
|
|
45839
|
+
access_token: token,
|
|
45840
|
+
refresh_token: "",
|
|
45841
|
+
access_expires_at: "9999-12-31T23:59:59.999Z"
|
|
45842
|
+
});
|
|
45843
|
+
} catch (err) {
|
|
45844
|
+
this.context.stderr.write(
|
|
45845
|
+
`keynv: failed to persist credentials: ${err instanceof Error ? err.message : String(err)}
|
|
45846
|
+
`
|
|
45847
|
+
);
|
|
45848
|
+
return 1;
|
|
45849
|
+
}
|
|
45850
|
+
this.context.stdout.write(`logged in as ${data.email} (${data.org_role})
|
|
45395
45851
|
`);
|
|
45396
45852
|
return 0;
|
|
45397
45853
|
}
|
|
@@ -45442,7 +45898,8 @@ var WhoamiCommand = class extends Command {
|
|
|
45442
45898
|
}
|
|
45443
45899
|
this.context.stdout.write(`user: ${data.email} (${data.id})
|
|
45444
45900
|
`);
|
|
45445
|
-
|
|
45901
|
+
const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
|
|
45902
|
+
this.context.stdout.write(`org: ${orgDisplay}
|
|
45446
45903
|
`);
|
|
45447
45904
|
this.context.stdout.write(`role: ${data.org_role}
|
|
45448
45905
|
`);
|
|
@@ -45451,7 +45908,8 @@ var WhoamiCommand = class extends Command {
|
|
|
45451
45908
|
} else {
|
|
45452
45909
|
this.context.stdout.write("memberships:\n");
|
|
45453
45910
|
for (const m of data.memberships) {
|
|
45454
|
-
|
|
45911
|
+
const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
|
|
45912
|
+
this.context.stdout.write(` ${proj}: ${m.role}
|
|
45455
45913
|
`);
|
|
45456
45914
|
}
|
|
45457
45915
|
}
|
|
@@ -45462,14 +45920,6 @@ var WhoamiCommand = class extends Command {
|
|
|
45462
45920
|
// src/commands/member.ts
|
|
45463
45921
|
init_http();
|
|
45464
45922
|
init_format();
|
|
45465
|
-
async function findProjectIdByName(client, name) {
|
|
45466
|
-
const data = await client.request(
|
|
45467
|
-
"/v1/projects"
|
|
45468
|
-
);
|
|
45469
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
45470
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
45471
|
-
return match.id;
|
|
45472
|
-
}
|
|
45473
45923
|
var MemberAddCommand = class extends Command {
|
|
45474
45924
|
static paths = [["member", "add"]];
|
|
45475
45925
|
static usage = Command.Usage({
|
|
@@ -45486,7 +45936,7 @@ var MemberAddCommand = class extends Command {
|
|
|
45486
45936
|
return 1;
|
|
45487
45937
|
}
|
|
45488
45938
|
const client = new ApiClient();
|
|
45489
|
-
const projectId2 = await
|
|
45939
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
45490
45940
|
await client.request(`/v1/projects/${projectId2}/members`, {
|
|
45491
45941
|
method: "POST",
|
|
45492
45942
|
body: { email: this.email, role: role2 }
|
|
@@ -45503,7 +45953,7 @@ var MemberRemoveCommand = class extends Command {
|
|
|
45503
45953
|
email = options_exports.String();
|
|
45504
45954
|
async execute() {
|
|
45505
45955
|
const client = new ApiClient();
|
|
45506
|
-
const projectId2 = await
|
|
45956
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
45507
45957
|
const members = await client.request(`/v1/projects/${projectId2}/members`);
|
|
45508
45958
|
const target = members.members.find((m) => m.email === this.email);
|
|
45509
45959
|
if (!target) {
|
|
@@ -45526,7 +45976,7 @@ var MemberListCommand = class extends Command {
|
|
|
45526
45976
|
json = options_exports.Boolean("--json", false);
|
|
45527
45977
|
async execute() {
|
|
45528
45978
|
const client = new ApiClient();
|
|
45529
|
-
const projectId2 = await
|
|
45979
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
45530
45980
|
const data = await client.request(`/v1/projects/${projectId2}/members`);
|
|
45531
45981
|
if (this.json) {
|
|
45532
45982
|
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
@@ -45547,114 +45997,6 @@ var MemberListCommand = class extends Command {
|
|
|
45547
45997
|
return 0;
|
|
45548
45998
|
}
|
|
45549
45999
|
};
|
|
45550
|
-
|
|
45551
|
-
// src/commands/project.ts
|
|
45552
|
-
init_http();
|
|
45553
|
-
init_format();
|
|
45554
|
-
var ProjectListCommand = class extends Command {
|
|
45555
|
-
static paths = [["project", "list"]];
|
|
45556
|
-
static usage = Command.Usage({
|
|
45557
|
-
description: "List projects visible to the current user."
|
|
45558
|
-
});
|
|
45559
|
-
json = options_exports.Boolean("--json", false);
|
|
45560
|
-
async execute() {
|
|
45561
|
-
const client = new ApiClient();
|
|
45562
|
-
const data = await client.request("/v1/projects");
|
|
45563
|
-
if (this.json) {
|
|
45564
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45565
|
-
`);
|
|
45566
|
-
return 0;
|
|
45567
|
-
}
|
|
45568
|
-
if (data.projects.length === 0) {
|
|
45569
|
-
this.context.stdout.write("no projects\n");
|
|
45570
|
-
return 0;
|
|
45571
|
-
}
|
|
45572
|
-
this.context.stdout.write(
|
|
45573
|
-
`${table(
|
|
45574
|
-
["name", "id", "created_at"],
|
|
45575
|
-
data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
|
|
45576
|
-
)}
|
|
45577
|
-
`
|
|
45578
|
-
);
|
|
45579
|
-
return 0;
|
|
45580
|
-
}
|
|
45581
|
-
};
|
|
45582
|
-
var ProjectCreateCommand = class extends Command {
|
|
45583
|
-
static paths = [["project", "create"]];
|
|
45584
|
-
static usage = Command.Usage({
|
|
45585
|
-
description: "Create a new project with one or more environments.",
|
|
45586
|
-
examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
|
|
45587
|
-
});
|
|
45588
|
-
name = options_exports.String();
|
|
45589
|
-
envs = options_exports.Array("--env", {
|
|
45590
|
-
description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
|
|
45591
|
-
});
|
|
45592
|
-
async execute() {
|
|
45593
|
-
const envs = (this.envs ?? ["dev"]).map((spec) => {
|
|
45594
|
-
const [name, tier, approval] = spec.split(":");
|
|
45595
|
-
return {
|
|
45596
|
-
name: name ?? "",
|
|
45597
|
-
tier: tier ?? "non-production",
|
|
45598
|
-
require_approval: approval === "approval"
|
|
45599
|
-
};
|
|
45600
|
-
});
|
|
45601
|
-
const client = new ApiClient();
|
|
45602
|
-
const result = await client.request("/v1/projects", {
|
|
45603
|
-
method: "POST",
|
|
45604
|
-
body: { name: this.name, environments: envs }
|
|
45605
|
-
});
|
|
45606
|
-
this.context.stdout.write(`created project ${result.name} (${result.id})
|
|
45607
|
-
`);
|
|
45608
|
-
for (const e2 of envs) {
|
|
45609
|
-
this.context.stdout.write(
|
|
45610
|
-
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45611
|
-
`
|
|
45612
|
-
);
|
|
45613
|
-
}
|
|
45614
|
-
return 0;
|
|
45615
|
-
}
|
|
45616
|
-
};
|
|
45617
|
-
var ProjectDescribeCommand = class extends Command {
|
|
45618
|
-
static paths = [["project", "describe"]];
|
|
45619
|
-
static usage = Command.Usage({ description: "Show metadata for one project." });
|
|
45620
|
-
id = options_exports.String();
|
|
45621
|
-
json = options_exports.Boolean("--json", false);
|
|
45622
|
-
async execute() {
|
|
45623
|
-
const client = new ApiClient();
|
|
45624
|
-
const data = await client.request(`/v1/projects/${this.id}`);
|
|
45625
|
-
if (this.json) {
|
|
45626
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45627
|
-
`);
|
|
45628
|
-
return 0;
|
|
45629
|
-
}
|
|
45630
|
-
this.context.stdout.write(`project: ${data.name} (${data.id})
|
|
45631
|
-
`);
|
|
45632
|
-
for (const e2 of data.environments) {
|
|
45633
|
-
this.context.stdout.write(
|
|
45634
|
-
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45635
|
-
`
|
|
45636
|
-
);
|
|
45637
|
-
}
|
|
45638
|
-
return 0;
|
|
45639
|
-
}
|
|
45640
|
-
};
|
|
45641
|
-
var ProjectDeleteCommand = class extends Command {
|
|
45642
|
-
static paths = [["project", "delete"]];
|
|
45643
|
-
static usage = Command.Usage({ description: "Soft-delete a project." });
|
|
45644
|
-
id = options_exports.String();
|
|
45645
|
-
force = options_exports.Boolean("--force", false);
|
|
45646
|
-
async execute() {
|
|
45647
|
-
if (!this.force) {
|
|
45648
|
-
this.context.stderr.write("keynv: refusing to delete without --force\n");
|
|
45649
|
-
return 2;
|
|
45650
|
-
}
|
|
45651
|
-
const client = new ApiClient();
|
|
45652
|
-
await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
|
|
45653
|
-
this.context.stdout.write(`deleted project ${this.id}
|
|
45654
|
-
`);
|
|
45655
|
-
return 0;
|
|
45656
|
-
}
|
|
45657
|
-
};
|
|
45658
46000
|
var RedactCommand = class extends Command {
|
|
45659
46001
|
static paths = [["redact"]];
|
|
45660
46002
|
static usage = Command.Usage({
|
|
@@ -45697,11 +46039,9 @@ var RedactStreamCommand = class extends Command {
|
|
|
45697
46039
|
static usage = Command.Usage({
|
|
45698
46040
|
description: "Stream stdin through the line-buffered redactor to stdout.",
|
|
45699
46041
|
details: `
|
|
45700
|
-
|
|
45701
|
-
|
|
45702
|
-
|
|
45703
|
-
line structure. Multi-line patterns are NOT applied here (see the
|
|
45704
|
-
streaming-mode limitation in @keynv/redactor).
|
|
46042
|
+
Pipe any output through the line-buffered redactor to stdout.
|
|
46043
|
+
Preserves original line structure. Multi-line patterns are NOT applied
|
|
46044
|
+
here (see the streaming-mode limitation in the redactor package).
|
|
45705
46045
|
`
|
|
45706
46046
|
});
|
|
45707
46047
|
async execute() {
|
|
@@ -45716,18 +46056,13 @@ streaming-mode limitation in @keynv/redactor).
|
|
|
45716
46056
|
// src/commands/secret.ts
|
|
45717
46057
|
init_dist();
|
|
45718
46058
|
init_http();
|
|
46059
|
+
init_secret();
|
|
45719
46060
|
init_format();
|
|
45720
|
-
init_tty();
|
|
45721
46061
|
init_cancel();
|
|
45722
46062
|
init_pickProject();
|
|
45723
46063
|
init_pickSecret();
|
|
45724
|
-
|
|
45725
|
-
|
|
45726
|
-
const data = await client.request("/v1/projects");
|
|
45727
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
45728
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
45729
|
-
return match.id;
|
|
45730
|
-
}
|
|
46064
|
+
init_tty();
|
|
46065
|
+
var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
|
|
45731
46066
|
function missingAlias(stderr) {
|
|
45732
46067
|
stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
|
|
45733
46068
|
return 1;
|
|
@@ -45776,7 +46111,8 @@ var SecretCreateCommand = class extends Command {
|
|
|
45776
46111
|
}
|
|
45777
46112
|
const parsed = parseAlias(alias2);
|
|
45778
46113
|
if (!parsed) {
|
|
45779
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46114
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46115
|
+
${ALIAS_FORMAT_HINT}
|
|
45780
46116
|
`);
|
|
45781
46117
|
return 1;
|
|
45782
46118
|
}
|
|
@@ -45789,7 +46125,7 @@ var SecretCreateCommand = class extends Command {
|
|
|
45789
46125
|
} else if (value === void 0) {
|
|
45790
46126
|
value = await promptHidden("value: ");
|
|
45791
46127
|
}
|
|
45792
|
-
const projectId2 = await
|
|
46128
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
45793
46129
|
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
45794
46130
|
method: "POST",
|
|
45795
46131
|
body: { env: parsed.environment, key: parsed.key, value }
|
|
@@ -45823,10 +46159,11 @@ var SecretGetCommand = class extends Command {
|
|
|
45823
46159
|
const parsed = parseAlias(alias2);
|
|
45824
46160
|
if (!parsed) {
|
|
45825
46161
|
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46162
|
+
${ALIAS_FORMAT_HINT}
|
|
45826
46163
|
`);
|
|
45827
46164
|
return 1;
|
|
45828
46165
|
}
|
|
45829
|
-
const projectId2 = await
|
|
46166
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
45830
46167
|
const data = await client.request(
|
|
45831
46168
|
`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
|
|
45832
46169
|
);
|
|
@@ -45867,7 +46204,7 @@ var SecretListCommand = class extends Command {
|
|
|
45867
46204
|
throw err;
|
|
45868
46205
|
}
|
|
45869
46206
|
}
|
|
45870
|
-
const projectId2 = await
|
|
46207
|
+
const projectId2 = await resolveProjectId(client, projectName);
|
|
45871
46208
|
const data = await client.request(`/v1/projects/${projectId2}/secrets`);
|
|
45872
46209
|
if (this.json) {
|
|
45873
46210
|
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
@@ -45911,6 +46248,7 @@ var SecretRotateCommand = class extends Command {
|
|
|
45911
46248
|
const parsed = parseAlias(alias2);
|
|
45912
46249
|
if (!parsed) {
|
|
45913
46250
|
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46251
|
+
${ALIAS_FORMAT_HINT}
|
|
45914
46252
|
`);
|
|
45915
46253
|
return 1;
|
|
45916
46254
|
}
|
|
@@ -45926,7 +46264,7 @@ var SecretRotateCommand = class extends Command {
|
|
|
45926
46264
|
} else {
|
|
45927
46265
|
value = await promptHidden("new value: ");
|
|
45928
46266
|
}
|
|
45929
|
-
const projectId2 = await
|
|
46267
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
45930
46268
|
const data = await client.request(
|
|
45931
46269
|
`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
|
|
45932
46270
|
{ method: "POST", body: { new_value: value } }
|
|
@@ -45957,10 +46295,11 @@ var SecretDeleteCommand = class extends Command {
|
|
|
45957
46295
|
const parsed = parseAlias(alias2);
|
|
45958
46296
|
if (!parsed) {
|
|
45959
46297
|
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46298
|
+
${ALIAS_FORMAT_HINT}
|
|
45960
46299
|
`);
|
|
45961
46300
|
return 1;
|
|
45962
46301
|
}
|
|
45963
|
-
const projectId2 = await
|
|
46302
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
45964
46303
|
await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
|
|
45965
46304
|
method: "DELETE"
|
|
45966
46305
|
});
|
|
@@ -45969,6 +46308,140 @@ var SecretDeleteCommand = class extends Command {
|
|
|
45969
46308
|
return 0;
|
|
45970
46309
|
}
|
|
45971
46310
|
};
|
|
46311
|
+
function generateSecret(bytes = 32) {
|
|
46312
|
+
return randomBytes(bytes).toString("base64");
|
|
46313
|
+
}
|
|
46314
|
+
var ServerInitCommand = class extends Command {
|
|
46315
|
+
static paths = [["server", "init"]];
|
|
46316
|
+
static usage = Command.Usage({
|
|
46317
|
+
description: "Generate secrets and deployment files for a new keynv server.",
|
|
46318
|
+
details: `
|
|
46319
|
+
Interactive onboarding wizard that generates the configuration
|
|
46320
|
+
a fresh keynv server needs: JWT signing secret, master encryption
|
|
46321
|
+
key, and deployment templates.
|
|
46322
|
+
|
|
46323
|
+
Outputs:
|
|
46324
|
+
\u2022 A .keynv-server.env file you feed into your deployment
|
|
46325
|
+
(Docker Compose, Coolify, or bare-metal).
|
|
46326
|
+
\u2022 Optionally a docker-compose.yml in the current directory.
|
|
46327
|
+
|
|
46328
|
+
Non-interactive with --yes to skip prompts.
|
|
46329
|
+
`,
|
|
46330
|
+
examples: [
|
|
46331
|
+
["Interactive wizard", "$0 server init"],
|
|
46332
|
+
["Non-interactive (CI/scripts)", "$0 server init --yes"],
|
|
46333
|
+
["Docker Compose output", "$0 server init --compose"],
|
|
46334
|
+
["Custom output path", "$0 server init --out ./config/env"]
|
|
46335
|
+
]
|
|
46336
|
+
});
|
|
46337
|
+
yes = options_exports.Boolean("--yes", false, {
|
|
46338
|
+
description: "Skip all prompts (non-interactive)."
|
|
46339
|
+
});
|
|
46340
|
+
compose = options_exports.Boolean("--compose", false, {
|
|
46341
|
+
description: "Also write a docker-compose.yml to the current directory."
|
|
46342
|
+
});
|
|
46343
|
+
out = options_exports.String("--out", {
|
|
46344
|
+
description: "Write server env file to this path (default: .keynv-server.env)."
|
|
46345
|
+
});
|
|
46346
|
+
async execute() {
|
|
46347
|
+
const jwtSecret = generateSecret(48);
|
|
46348
|
+
const masterKey = generateSecret(32);
|
|
46349
|
+
const webSessionSecret = generateSecret(48);
|
|
46350
|
+
const outPath = this.out ?? ".keynv-server.env";
|
|
46351
|
+
const contents = [
|
|
46352
|
+
"# Generated by `keynv server init`.",
|
|
46353
|
+
"# Keep this file secret \u2014 it contains cryptographic key material.",
|
|
46354
|
+
`KEYNV_JWT_SECRET=${jwtSecret}`,
|
|
46355
|
+
`KEYNV_MASTER_KEY=${masterKey}`,
|
|
46356
|
+
"",
|
|
46357
|
+
"# Web dashboard session secret (required for Next.js JWE session tokens).",
|
|
46358
|
+
`KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
|
|
46359
|
+
"",
|
|
46360
|
+
"# Server configuration (uncomment and adjust):",
|
|
46361
|
+
"# KEYNV_PORT=8080",
|
|
46362
|
+
"# KEYNV_WEB_URL=https://keynv.example.com",
|
|
46363
|
+
"# KEYNV_PUBLIC_REGISTRATION=false",
|
|
46364
|
+
"# KEYNV_DB_PATH=./keynv.db",
|
|
46365
|
+
"# KEYNV_MASTER_KEY_FILE=./master.key",
|
|
46366
|
+
"# KEYNV_RATE_LIMIT_PER_MINUTE=120",
|
|
46367
|
+
"# KEYNV_ARGON2_MEMORY_KIB=46080",
|
|
46368
|
+
"# KEYNV_ARGON2_TIME_COST=3"
|
|
46369
|
+
].join("\n");
|
|
46370
|
+
const { writeFileSync: writeFileSync4 } = await import('fs');
|
|
46371
|
+
writeFileSync4(outPath, `${contents}
|
|
46372
|
+
`);
|
|
46373
|
+
this.context.stdout.write(`Wrote ${outPath}
|
|
46374
|
+
|
|
46375
|
+
`);
|
|
46376
|
+
this.context.stdout.write("Generated secrets:\n");
|
|
46377
|
+
this.context.stdout.write(
|
|
46378
|
+
` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
46379
|
+
`
|
|
46380
|
+
);
|
|
46381
|
+
this.context.stdout.write(
|
|
46382
|
+
` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
|
|
46383
|
+
`
|
|
46384
|
+
);
|
|
46385
|
+
this.context.stdout.write(
|
|
46386
|
+
` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
46387
|
+
`
|
|
46388
|
+
);
|
|
46389
|
+
this.context.stdout.write("\nNext steps:\n");
|
|
46390
|
+
this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
|
|
46391
|
+
`);
|
|
46392
|
+
this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
|
|
46393
|
+
this.context.stdout.write(" or load it into Coolify as environment variables.\n");
|
|
46394
|
+
this.context.stdout.write(" 2. Bootstrap the first owner:\n");
|
|
46395
|
+
this.context.stdout.write(
|
|
46396
|
+
" docker exec -it <server-container> node dist/bootstrap.js \\\n"
|
|
46397
|
+
);
|
|
46398
|
+
this.context.stdout.write(" --owner-email alice@example.com \\\n");
|
|
46399
|
+
this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
|
|
46400
|
+
this.context.stdout.write(' --org-name "Acme Inc"\n');
|
|
46401
|
+
this.context.stdout.write(" 3. Install the CLI and log in:\n");
|
|
46402
|
+
this.context.stdout.write(" npm install -g @keynv/cli\n");
|
|
46403
|
+
this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
|
|
46404
|
+
this.context.stdout.write(" 4. Onboard your first project:\n");
|
|
46405
|
+
this.context.stdout.write(" cd your-project && keynv init\n");
|
|
46406
|
+
if (this.compose) {
|
|
46407
|
+
const composeContent = `# keynv server + Litestream sidecar
|
|
46408
|
+
# Generated by \`keynv server init\`.
|
|
46409
|
+
# Load with: docker compose --env-file .keynv-server.env up -d
|
|
46410
|
+
|
|
46411
|
+
services:
|
|
46412
|
+
server:
|
|
46413
|
+
image: ghcr.io/keynv-labs/keynv-server:latest
|
|
46414
|
+
restart: unless-stopped
|
|
46415
|
+
ports:
|
|
46416
|
+
- '8080:8080'
|
|
46417
|
+
env_file:
|
|
46418
|
+
- .keynv-server.env
|
|
46419
|
+
volumes:
|
|
46420
|
+
- keynv-data:/data
|
|
46421
|
+
|
|
46422
|
+
litestream:
|
|
46423
|
+
image: litestream/litestream:latest
|
|
46424
|
+
restart: unless-stopped
|
|
46425
|
+
volumes:
|
|
46426
|
+
- keynv-data:/data
|
|
46427
|
+
command: replicate
|
|
46428
|
+
environment:
|
|
46429
|
+
LITESTREAM_DB_PATH: /data/keynv.db
|
|
46430
|
+
LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
|
|
46431
|
+
AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
|
|
46432
|
+
AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
|
|
46433
|
+
|
|
46434
|
+
volumes:
|
|
46435
|
+
keynv-data:
|
|
46436
|
+
`;
|
|
46437
|
+
writeFileSync4("docker-compose.yml", composeContent);
|
|
46438
|
+
this.context.stdout.write(
|
|
46439
|
+
"\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
|
|
46440
|
+
);
|
|
46441
|
+
}
|
|
46442
|
+
return 0;
|
|
46443
|
+
}
|
|
46444
|
+
};
|
|
45972
46445
|
|
|
45973
46446
|
// src/commands/test.ts
|
|
45974
46447
|
init_dist();
|
|
@@ -46321,18 +46794,9 @@ var TESTERS = [
|
|
|
46321
46794
|
sshTester,
|
|
46322
46795
|
httpTester
|
|
46323
46796
|
];
|
|
46324
|
-
function findTester(type) {
|
|
46325
|
-
return TESTERS.find((t) => t.type === type) ?? null;
|
|
46326
|
-
}
|
|
46327
46797
|
|
|
46328
46798
|
// src/commands/test.ts
|
|
46329
46799
|
init_http();
|
|
46330
|
-
async function findProjectIdByName3(client, name) {
|
|
46331
|
-
const data = await client.request("/v1/projects");
|
|
46332
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
46333
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
46334
|
-
return match.id;
|
|
46335
|
-
}
|
|
46336
46800
|
function parseTargets(specs) {
|
|
46337
46801
|
const out = {};
|
|
46338
46802
|
for (const spec of specs) {
|
|
@@ -46386,12 +46850,17 @@ the error message.
|
|
|
46386
46850
|
);
|
|
46387
46851
|
return 2;
|
|
46388
46852
|
}
|
|
46389
|
-
const tester =
|
|
46853
|
+
const tester = TESTERS.find((t) => t.type === this.as);
|
|
46390
46854
|
if (!tester) {
|
|
46391
46855
|
this.context.stderr.write(`keynv: unknown tester '${this.as}'
|
|
46392
46856
|
`);
|
|
46393
46857
|
return 1;
|
|
46394
46858
|
}
|
|
46859
|
+
const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
|
|
46860
|
+
if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
|
|
46861
|
+
this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
|
|
46862
|
+
return 2;
|
|
46863
|
+
}
|
|
46395
46864
|
let target;
|
|
46396
46865
|
try {
|
|
46397
46866
|
target = parseTargets(this.targets ?? []);
|
|
@@ -46408,7 +46877,7 @@ the error message.
|
|
|
46408
46877
|
}
|
|
46409
46878
|
let value;
|
|
46410
46879
|
try {
|
|
46411
|
-
const projectId2 = await
|
|
46880
|
+
const projectId2 = await resolveProjectId(client, parsedAlias.project);
|
|
46412
46881
|
const data = await client.request(
|
|
46413
46882
|
`/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
|
|
46414
46883
|
);
|
|
@@ -46419,11 +46888,6 @@ the error message.
|
|
|
46419
46888
|
`);
|
|
46420
46889
|
return 1;
|
|
46421
46890
|
}
|
|
46422
|
-
const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
|
|
46423
|
-
if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
|
|
46424
|
-
this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
|
|
46425
|
-
return 2;
|
|
46426
|
-
}
|
|
46427
46891
|
const result = await runTest({
|
|
46428
46892
|
tester,
|
|
46429
46893
|
secret: { alias: parsedAlias.literal, value },
|
|
@@ -46465,25 +46929,6 @@ the error message.
|
|
|
46465
46929
|
}
|
|
46466
46930
|
};
|
|
46467
46931
|
|
|
46468
|
-
// src/commands/ui.ts
|
|
46469
|
-
init_menu();
|
|
46470
|
-
init_tty();
|
|
46471
|
-
var UICommand = class extends Command {
|
|
46472
|
-
static paths = [["ui"]];
|
|
46473
|
-
static usage = Command.Usage({
|
|
46474
|
-
description: "Open the interactive menu (also runs by default when no args are given)."
|
|
46475
|
-
});
|
|
46476
|
-
async execute() {
|
|
46477
|
-
if (!isInteractive()) {
|
|
46478
|
-
this.context.stdout.write(
|
|
46479
|
-
"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"
|
|
46480
|
-
);
|
|
46481
|
-
return 0;
|
|
46482
|
-
}
|
|
46483
|
-
return runMenu();
|
|
46484
|
-
}
|
|
46485
|
-
};
|
|
46486
|
-
|
|
46487
46932
|
// src/index.ts
|
|
46488
46933
|
init_format();
|
|
46489
46934
|
init_version();
|
|
@@ -46517,7 +46962,7 @@ cli.register(InitCommand);
|
|
|
46517
46962
|
cli.register(RedactCommand);
|
|
46518
46963
|
cli.register(RedactStreamCommand);
|
|
46519
46964
|
cli.register(TestCommand);
|
|
46520
|
-
cli.register(
|
|
46965
|
+
cli.register(ServerInitCommand);
|
|
46521
46966
|
var argv = process.argv.slice(2);
|
|
46522
46967
|
if (argv.length === 0) {
|
|
46523
46968
|
const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));
|