@keynv/cli 0.1.0-rc.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1773 -1065
- 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
|
|
1059
|
+
VERSION = "0.1.0" ;
|
|
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 = [];
|
|
@@ -5415,6 +5415,12 @@ var init_payload_schemas = __esm({
|
|
|
5415
5415
|
}).strict(),
|
|
5416
5416
|
"project.deleted": external_exports.object({ project_id: projectId, name: external_exports.string() }).strict(),
|
|
5417
5417
|
"project.dek_rotated": external_exports.object({ project_id: projectId }).strict(),
|
|
5418
|
+
"environment.created": external_exports.object({
|
|
5419
|
+
project_id: projectId,
|
|
5420
|
+
environment: external_exports.string().min(1),
|
|
5421
|
+
tier: external_exports.enum(["production", "non-production"]),
|
|
5422
|
+
require_approval: external_exports.boolean()
|
|
5423
|
+
}).strict(),
|
|
5418
5424
|
"member.added": external_exports.object({ project_id: projectId, target_user_id: userId, role }).strict(),
|
|
5419
5425
|
"member.removed": external_exports.object({ project_id: projectId, target_user_id: userId }).strict(),
|
|
5420
5426
|
"member.role_changed": external_exports.object({ project_id: projectId, target_user_id: userId, role }).strict(),
|
|
@@ -5530,7 +5536,15 @@ function clearCredentialsFile() {
|
|
|
5530
5536
|
if (existsSync(legacy)) rmSync(legacy, { force: true });
|
|
5531
5537
|
try {
|
|
5532
5538
|
entry().deletePassword();
|
|
5533
|
-
|
|
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;
|
|
5534
5548
|
}
|
|
5535
5549
|
}
|
|
5536
5550
|
var SERVICE, KEY_ACCOUNT;
|
|
@@ -5573,8 +5587,9 @@ async function saveCredentials(creds) {
|
|
|
5573
5587
|
cache = creds;
|
|
5574
5588
|
}
|
|
5575
5589
|
function clearCredentials() {
|
|
5576
|
-
clearCredentialsFile();
|
|
5590
|
+
const ok = clearCredentialsFile();
|
|
5577
5591
|
cache = null;
|
|
5592
|
+
return ok;
|
|
5578
5593
|
}
|
|
5579
5594
|
var cache;
|
|
5580
5595
|
var init_store = __esm({
|
|
@@ -5584,6 +5599,9 @@ var init_store = __esm({
|
|
|
5584
5599
|
});
|
|
5585
5600
|
|
|
5586
5601
|
// src/client/http.ts
|
|
5602
|
+
function isClientError(err) {
|
|
5603
|
+
return err instanceof Error && typeof err.status === "number";
|
|
5604
|
+
}
|
|
5587
5605
|
function clientError(status, code, message, details) {
|
|
5588
5606
|
const err = Object.assign(new Error(message), { status, code, details });
|
|
5589
5607
|
return err;
|
|
@@ -5598,6 +5616,7 @@ function buildUrl(base, path, query) {
|
|
|
5598
5616
|
return url.toString();
|
|
5599
5617
|
}
|
|
5600
5618
|
async function tryRefresh(creds) {
|
|
5619
|
+
if (creds.auth_kind === "cli_token" || creds.refresh_token.length === 0) return null;
|
|
5601
5620
|
const res = await fetch(buildUrl(creds.server_url, "/v1/auth/refresh"), {
|
|
5602
5621
|
method: "POST",
|
|
5603
5622
|
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
@@ -5637,7 +5656,16 @@ var init_http = __esm({
|
|
|
5637
5656
|
return this.creds;
|
|
5638
5657
|
}
|
|
5639
5658
|
async ensureHydrated() {
|
|
5640
|
-
if (this.hydrated)
|
|
5659
|
+
if (this.hydrated) {
|
|
5660
|
+
try {
|
|
5661
|
+
await this.hydrated;
|
|
5662
|
+
} catch (err) {
|
|
5663
|
+
process.stderr.write(
|
|
5664
|
+
`${err instanceof Error ? err.message : `keynv: credential load failed \u2014 ${String(err)}`}
|
|
5665
|
+
`
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5641
5669
|
}
|
|
5642
5670
|
async setCredentials(creds) {
|
|
5643
5671
|
this.creds = creds;
|
|
@@ -5659,21 +5687,40 @@ var init_http = __esm({
|
|
|
5659
5687
|
headers.authorization = `Bearer ${this.creds.access_token}`;
|
|
5660
5688
|
}
|
|
5661
5689
|
const url = this.creds ? buildUrl(this.creds.server_url, path, opts.query) : path;
|
|
5662
|
-
let res
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5690
|
+
let res;
|
|
5691
|
+
try {
|
|
5692
|
+
res = await fetch(url, {
|
|
5693
|
+
method: opts.method ?? "GET",
|
|
5694
|
+
headers,
|
|
5695
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
5696
|
+
});
|
|
5697
|
+
} catch (err) {
|
|
5698
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
5699
|
+
const serverUrl = this.creds?.server_url ?? url;
|
|
5700
|
+
throw new Error(
|
|
5701
|
+
`keynv: cannot reach server '${serverUrl}' \u2014 ${cause}
|
|
5702
|
+
Check the server is running: curl ${serverUrl}/v1/health`
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5667
5705
|
if (res.status === 401 && this.creds && opts.authed !== false) {
|
|
5668
5706
|
const refreshed = await tryRefresh(this.creds);
|
|
5669
5707
|
if (refreshed) {
|
|
5670
5708
|
this.creds = refreshed;
|
|
5671
5709
|
headers.authorization = `Bearer ${refreshed.access_token}`;
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5710
|
+
try {
|
|
5711
|
+
res = await fetch(url, {
|
|
5712
|
+
method: opts.method ?? "GET",
|
|
5713
|
+
headers,
|
|
5714
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
5715
|
+
});
|
|
5716
|
+
} catch (err) {
|
|
5717
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
5718
|
+
const serverUrl = this.creds?.server_url ?? url;
|
|
5719
|
+
throw new Error(
|
|
5720
|
+
`keynv: cannot reach server '${serverUrl}' \u2014 ${cause}
|
|
5721
|
+
Check the server is running: curl ${serverUrl}/v1/health`
|
|
5722
|
+
);
|
|
5723
|
+
}
|
|
5677
5724
|
}
|
|
5678
5725
|
}
|
|
5679
5726
|
if (res.status === 204) return void 0;
|
|
@@ -5712,8 +5759,38 @@ function fmtError(err) {
|
|
|
5712
5759
|
const status = err.status ? ` (${err.status})` : "";
|
|
5713
5760
|
return `keynv:${code}${status} ${err.message}`;
|
|
5714
5761
|
}
|
|
5762
|
+
function handleExecError(stderr, err) {
|
|
5763
|
+
if (isClientError(err)) {
|
|
5764
|
+
const code = err.code ? `[${err.code}] ` : "";
|
|
5765
|
+
stderr.write(`keynv: ${code}${err.message}
|
|
5766
|
+
`);
|
|
5767
|
+
} else if (err instanceof Error) {
|
|
5768
|
+
stderr.write(`keynv: ${err.message}
|
|
5769
|
+
`);
|
|
5770
|
+
} else {
|
|
5771
|
+
stderr.write(`keynv: unexpected error
|
|
5772
|
+
`);
|
|
5773
|
+
}
|
|
5774
|
+
return 1;
|
|
5775
|
+
}
|
|
5715
5776
|
var init_format = __esm({
|
|
5716
5777
|
"src/ui/format.ts"() {
|
|
5778
|
+
init_http();
|
|
5779
|
+
}
|
|
5780
|
+
});
|
|
5781
|
+
function walkUp(startDir, predicate) {
|
|
5782
|
+
let dir = resolve(startDir);
|
|
5783
|
+
for (let i = 0; i < 64; i++) {
|
|
5784
|
+
const result = predicate(dir);
|
|
5785
|
+
if (result !== null && result !== void 0) return result;
|
|
5786
|
+
const parent = dirname(dir);
|
|
5787
|
+
if (parent === dir) return null;
|
|
5788
|
+
dir = parent;
|
|
5789
|
+
}
|
|
5790
|
+
return null;
|
|
5791
|
+
}
|
|
5792
|
+
var init_fs = __esm({
|
|
5793
|
+
"src/util/fs.ts"() {
|
|
5717
5794
|
}
|
|
5718
5795
|
});
|
|
5719
5796
|
function parseEnvFile(content, filename) {
|
|
@@ -5742,7 +5819,7 @@ function parseEnvFile(content, filename) {
|
|
|
5742
5819
|
`invalid key '${name}' (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
|
|
5743
5820
|
);
|
|
5744
5821
|
}
|
|
5745
|
-
|
|
5822
|
+
const valueRaw = body.slice(eq + 1);
|
|
5746
5823
|
let value;
|
|
5747
5824
|
const valueLeading = valueRaw.replace(/^\s+/, "");
|
|
5748
5825
|
const firstCh = valueLeading.charAt(0);
|
|
@@ -5774,20 +5851,14 @@ function parseEnvFile(content, filename) {
|
|
|
5774
5851
|
return entries;
|
|
5775
5852
|
}
|
|
5776
5853
|
function findEnvFile(startDir) {
|
|
5777
|
-
|
|
5778
|
-
for (let i = 0; i < 64; i++) {
|
|
5854
|
+
return walkUp(startDir, (dir) => {
|
|
5779
5855
|
const candidate = join(dir, ENV_FILE_BASENAME);
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
} catch {
|
|
5784
|
-
}
|
|
5856
|
+
try {
|
|
5857
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
5858
|
+
} catch {
|
|
5785
5859
|
}
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
dir = parent;
|
|
5789
|
-
}
|
|
5790
|
-
return null;
|
|
5860
|
+
return null;
|
|
5861
|
+
});
|
|
5791
5862
|
}
|
|
5792
5863
|
function loadEnvFile(opts) {
|
|
5793
5864
|
if (opts.disabled) return null;
|
|
@@ -5812,6 +5883,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
|
|
|
5812
5883
|
var init_envFile = __esm({
|
|
5813
5884
|
"src/exec/envFile.ts"() {
|
|
5814
5885
|
init_dist();
|
|
5886
|
+
init_fs();
|
|
5815
5887
|
MAX_FILE_BYTES = 1e6;
|
|
5816
5888
|
KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5817
5889
|
ENV_FILE_BASENAME = ".keynv.env";
|
|
@@ -5848,16 +5920,129 @@ var init_envFile = __esm({
|
|
|
5848
5920
|
}
|
|
5849
5921
|
});
|
|
5850
5922
|
|
|
5851
|
-
// src/
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5923
|
+
// src/init/heuristics.ts
|
|
5924
|
+
function shannonEntropyBits(s) {
|
|
5925
|
+
if (s.length === 0) return 0;
|
|
5926
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5927
|
+
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
5928
|
+
let h2 = 0;
|
|
5929
|
+
for (const c2 of counts.values()) {
|
|
5930
|
+
const p2 = c2 / s.length;
|
|
5931
|
+
h2 -= p2 * Math.log2(p2);
|
|
5932
|
+
}
|
|
5933
|
+
return h2;
|
|
5858
5934
|
}
|
|
5859
|
-
|
|
5860
|
-
|
|
5935
|
+
function nameMatchesLiteralPrefix(name) {
|
|
5936
|
+
const upper = name.toUpperCase();
|
|
5937
|
+
return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
|
|
5938
|
+
}
|
|
5939
|
+
function nameMatchesSecretSuffix(name) {
|
|
5940
|
+
const upper = name.toUpperCase();
|
|
5941
|
+
for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
|
|
5942
|
+
if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
|
|
5943
|
+
return { matched: true, hint };
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
return { matched: false, hint: "" };
|
|
5947
|
+
}
|
|
5948
|
+
function classifyEntry(name, value) {
|
|
5949
|
+
const upper = name.toUpperCase();
|
|
5950
|
+
if (NAME_FRAMEWORK_MANAGED.has(upper)) {
|
|
5951
|
+
return { verdict: "skip", hint: "framework/shell-managed" };
|
|
5952
|
+
}
|
|
5953
|
+
if (NAME_LITERAL_EXACT.has(upper)) {
|
|
5954
|
+
return { verdict: "literal", hint: "common config var" };
|
|
5955
|
+
}
|
|
5956
|
+
if (nameMatchesLiteralPrefix(name)) {
|
|
5957
|
+
return { verdict: "literal", hint: "public env, build-time bundled" };
|
|
5958
|
+
}
|
|
5959
|
+
if (value.length === 0) {
|
|
5960
|
+
return { verdict: "literal", hint: "empty" };
|
|
5961
|
+
}
|
|
5962
|
+
for (const { re, hint } of VALUE_PATTERNS) {
|
|
5963
|
+
if (re.test(value)) return { verdict: "secret", hint };
|
|
5964
|
+
}
|
|
5965
|
+
const suffix = nameMatchesSecretSuffix(name);
|
|
5966
|
+
if (suffix.matched) {
|
|
5967
|
+
return { verdict: "secret", hint: suffix.hint };
|
|
5968
|
+
}
|
|
5969
|
+
if (NAME_DB_URL.test(name)) {
|
|
5970
|
+
return { verdict: "secret", hint: "database URL" };
|
|
5971
|
+
}
|
|
5972
|
+
if (value.length >= 32) {
|
|
5973
|
+
const bits = shannonEntropyBits(value);
|
|
5974
|
+
if (bits >= 3.5) {
|
|
5975
|
+
return { verdict: "secret", hint: `${value.length}-char random-looking string` };
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
return { verdict: "ambiguous", hint: "" };
|
|
5979
|
+
}
|
|
5980
|
+
function previewValue(value, max = 40) {
|
|
5981
|
+
if (value.length === 0) return "(empty)";
|
|
5982
|
+
if (value.length <= max) return value;
|
|
5983
|
+
return `${value.slice(0, max - 1)}\u2026`;
|
|
5984
|
+
}
|
|
5985
|
+
var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
|
|
5986
|
+
var init_heuristics = __esm({
|
|
5987
|
+
"src/init/heuristics.ts"() {
|
|
5988
|
+
NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
|
|
5989
|
+
"NODE_ENV",
|
|
5990
|
+
"PORT",
|
|
5991
|
+
"HOST",
|
|
5992
|
+
"HOSTNAME",
|
|
5993
|
+
"PATH",
|
|
5994
|
+
"HOME",
|
|
5995
|
+
"USER",
|
|
5996
|
+
"SHELL",
|
|
5997
|
+
"PWD",
|
|
5998
|
+
"CI"
|
|
5999
|
+
]);
|
|
6000
|
+
NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
|
|
6001
|
+
"DEBUG",
|
|
6002
|
+
"LOG_LEVEL",
|
|
6003
|
+
"LOGLEVEL",
|
|
6004
|
+
"TZ",
|
|
6005
|
+
"LANG",
|
|
6006
|
+
"LC_ALL",
|
|
6007
|
+
"NODE_OPTIONS",
|
|
6008
|
+
"NPM_CONFIG_LOGLEVEL",
|
|
6009
|
+
"TS_NODE_PROJECT"
|
|
6010
|
+
]);
|
|
6011
|
+
NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
|
|
6012
|
+
NAME_SECRET_SUFFIXES = [
|
|
6013
|
+
{ suffix: "PRIVATE_KEY", hint: "private key" },
|
|
6014
|
+
{ suffix: "API_KEY", hint: "API key" },
|
|
6015
|
+
{ suffix: "ACCESS_KEY", hint: "access key" },
|
|
6016
|
+
{ suffix: "SECRET_KEY", hint: "secret key" },
|
|
6017
|
+
{ suffix: "SECRET", hint: "secret" },
|
|
6018
|
+
{ suffix: "PASSWORD", hint: "password" },
|
|
6019
|
+
{ suffix: "PASSPHRASE", hint: "passphrase" },
|
|
6020
|
+
{ suffix: "TOKEN", hint: "token" },
|
|
6021
|
+
{ suffix: "KEY", hint: "key" },
|
|
6022
|
+
{ suffix: "CREDENTIALS", hint: "credentials" },
|
|
6023
|
+
{ suffix: "AUTH", hint: "auth" },
|
|
6024
|
+
{ suffix: "DSN", hint: "connection string" }
|
|
6025
|
+
];
|
|
6026
|
+
NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
|
|
6027
|
+
VALUE_PATTERNS = [
|
|
6028
|
+
{ re: /^sk-proj-/, hint: "OpenAI project key" },
|
|
6029
|
+
{ re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
|
|
6030
|
+
{ re: /^sk_live_/, hint: "Stripe live key" },
|
|
6031
|
+
{ re: /^sk_test_/, hint: "Stripe test key" },
|
|
6032
|
+
{ re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
|
|
6033
|
+
{ re: /^xoxb-/, hint: "Slack bot token" },
|
|
6034
|
+
{ re: /^xoxp-/, hint: "Slack user token" },
|
|
6035
|
+
{ re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
|
|
6036
|
+
{ re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
|
|
6037
|
+
{ re: /^gho_/, hint: "GitHub OAuth token" },
|
|
6038
|
+
{ re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
|
|
6039
|
+
{ re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
|
|
6040
|
+
{ re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
|
|
6041
|
+
{ re: /^ya29\./, hint: "Google OAuth access token" },
|
|
6042
|
+
{ re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
|
|
6043
|
+
{ re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
|
|
6044
|
+
{ re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
|
|
6045
|
+
];
|
|
5861
6046
|
}
|
|
5862
6047
|
});
|
|
5863
6048
|
|
|
@@ -7015,23 +7200,109 @@ ${c2}
|
|
|
7015
7200
|
} }).prompt();
|
|
7016
7201
|
}
|
|
7017
7202
|
});
|
|
7203
|
+
function aiContextBody() {
|
|
7204
|
+
return `## keynv (secrets)
|
|
7205
|
+
|
|
7206
|
+
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.
|
|
7207
|
+
|
|
7208
|
+
### Mental model
|
|
7209
|
+
|
|
7210
|
+
- **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
|
|
7211
|
+
- **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
|
|
7212
|
+
- **\`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.
|
|
7213
|
+
- **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.
|
|
7214
|
+
|
|
7215
|
+
### What you should do
|
|
7216
|
+
|
|
7217
|
+
When the user asks about secrets or env vars, follow this decision tree:
|
|
7218
|
+
|
|
7219
|
+
| User intent | Run / suggest |
|
|
7220
|
+
|---|---|
|
|
7221
|
+
| "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
|
|
7222
|
+
| "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
|
|
7223
|
+
| "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
|
|
7224
|
+
| "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
|
|
7225
|
+
| "Rotate this key" | \`keynv secret rotate @alias\` |
|
|
7226
|
+
| "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\`) |
|
|
7227
|
+
| "Who has access?" | \`keynv member list <project>\` |
|
|
7228
|
+
|
|
7229
|
+
### Hard rules \u2014 do not violate
|
|
7230
|
+
|
|
7231
|
+
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.
|
|
7232
|
+
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.
|
|
7233
|
+
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.
|
|
7234
|
+
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.
|
|
7235
|
+
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.
|
|
7236
|
+
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.
|
|
7237
|
+
|
|
7238
|
+
### Quick reference
|
|
7239
|
+
|
|
7240
|
+
\`\`\`bash
|
|
7241
|
+
keynv # interactive TUI menu (pick projects, secrets, members)
|
|
7242
|
+
keynv exec -- <cmd> # run cmd with .keynv.env loaded
|
|
7243
|
+
keynv secret create # walk through creating a new secret
|
|
7244
|
+
keynv secret list <project> # list aliases (no values)
|
|
7245
|
+
keynv whoami # who am I logged in as
|
|
7246
|
+
keynv --help # full command list
|
|
7247
|
+
\`\`\`
|
|
7248
|
+
|
|
7249
|
+
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\`.`;
|
|
7250
|
+
}
|
|
7251
|
+
function renderKeynvBlock() {
|
|
7252
|
+
return `${KEYNV_BLOCK_START}
|
|
7253
|
+
${aiContextBody()}
|
|
7254
|
+
${KEYNV_BLOCK_END}`;
|
|
7255
|
+
}
|
|
7256
|
+
function writeAiContext(rootPath) {
|
|
7257
|
+
const path = join(rootPath, AGENTS_FILE_BASENAME);
|
|
7258
|
+
const block = renderKeynvBlock();
|
|
7259
|
+
if (!existsSync(path)) {
|
|
7260
|
+
const initial = `# Agent guidance for this project
|
|
7261
|
+
|
|
7262
|
+
${block}
|
|
7263
|
+
`;
|
|
7264
|
+
writeFileSync(path, initial);
|
|
7265
|
+
return "created";
|
|
7266
|
+
}
|
|
7267
|
+
const existing = readFileSync(path, "utf8");
|
|
7268
|
+
const startIdx = existing.indexOf(KEYNV_BLOCK_START);
|
|
7269
|
+
const endIdx = existing.indexOf(KEYNV_BLOCK_END);
|
|
7270
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
7271
|
+
const before = existing.slice(0, startIdx);
|
|
7272
|
+
const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
|
|
7273
|
+
const next = `${before}${block}${after}`;
|
|
7274
|
+
if (next === existing) return "unchanged";
|
|
7275
|
+
writeFileSync(path, next);
|
|
7276
|
+
return "updated";
|
|
7277
|
+
}
|
|
7278
|
+
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
7279
|
+
const trailingNewline = existing.endsWith("\n") ? "" : "\n";
|
|
7280
|
+
writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
|
|
7281
|
+
`);
|
|
7282
|
+
return "appended";
|
|
7283
|
+
}
|
|
7284
|
+
var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
|
|
7285
|
+
var init_aiContext = __esm({
|
|
7286
|
+
"src/init/aiContext.ts"() {
|
|
7287
|
+
AGENTS_FILE_BASENAME = "AGENTS.md";
|
|
7288
|
+
KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
|
|
7289
|
+
KEYNV_BLOCK_END = "<!-- keynv:end -->";
|
|
7290
|
+
}
|
|
7291
|
+
});
|
|
7018
7292
|
function findProjectRoot(startDir) {
|
|
7019
|
-
|
|
7020
|
-
for (let i = 0; i < 64; i++) {
|
|
7293
|
+
const result = walkUp(startDir, (dir) => {
|
|
7021
7294
|
for (const marker of PROJECT_MARKERS) {
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
return buildRoot(dir, marker);
|
|
7295
|
+
if (existsSync(join(dir, marker))) {
|
|
7296
|
+
return { dir, marker };
|
|
7025
7297
|
}
|
|
7026
7298
|
}
|
|
7027
7299
|
if (existsSync(join(dir, GIT_MARKER))) {
|
|
7028
|
-
return
|
|
7300
|
+
return { dir, marker: GIT_MARKER };
|
|
7029
7301
|
}
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
return null;
|
|
7302
|
+
return null;
|
|
7303
|
+
});
|
|
7304
|
+
if (!result) return null;
|
|
7305
|
+
return buildRoot(result.dir, result.marker);
|
|
7035
7306
|
}
|
|
7036
7307
|
function buildRoot(dir, marker) {
|
|
7037
7308
|
let suggestedName = basename(dir);
|
|
@@ -7045,16 +7316,20 @@ function buildRoot(dir, marker) {
|
|
|
7045
7316
|
}
|
|
7046
7317
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
7047
7318
|
scripts = Object.fromEntries(
|
|
7048
|
-
Object.entries(pkg.scripts).filter(
|
|
7049
|
-
([, v2]) => typeof v2 === "string"
|
|
7050
|
-
)
|
|
7319
|
+
Object.entries(pkg.scripts).filter(([, v2]) => typeof v2 === "string")
|
|
7051
7320
|
);
|
|
7052
7321
|
}
|
|
7053
7322
|
} catch {
|
|
7054
7323
|
invalid = true;
|
|
7055
7324
|
}
|
|
7056
7325
|
}
|
|
7057
|
-
return {
|
|
7326
|
+
return {
|
|
7327
|
+
path: dir,
|
|
7328
|
+
suggestedName,
|
|
7329
|
+
marker,
|
|
7330
|
+
packageJsonScripts: scripts,
|
|
7331
|
+
packageJsonInvalid: invalid
|
|
7332
|
+
};
|
|
7058
7333
|
}
|
|
7059
7334
|
function findEnvFiles(rootDir) {
|
|
7060
7335
|
let entries;
|
|
@@ -7112,6 +7387,7 @@ function hasExistingKeynvEnv(rootDir) {
|
|
|
7112
7387
|
var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
|
|
7113
7388
|
var init_detect = __esm({
|
|
7114
7389
|
"src/init/detect.ts"() {
|
|
7390
|
+
init_fs();
|
|
7115
7391
|
PROJECT_MARKERS = [
|
|
7116
7392
|
"package.json",
|
|
7117
7393
|
"pyproject.toml",
|
|
@@ -7124,220 +7400,10 @@ var init_detect = __esm({
|
|
|
7124
7400
|
];
|
|
7125
7401
|
GIT_MARKER = ".git";
|
|
7126
7402
|
ENV_GLOB = /^\.env(\.[A-Za-z0-9_-]+)?$/;
|
|
7127
|
-
ENV_EXAMPLE = /^\.env\.(example|sample|template)$/;
|
|
7403
|
+
ENV_EXAMPLE = /^\.env\.(example|sample|template|dist|defaults)$/;
|
|
7128
7404
|
KEYNV_ENV_BASENAME = ".keynv.env";
|
|
7129
7405
|
}
|
|
7130
7406
|
});
|
|
7131
|
-
function aiContextBody() {
|
|
7132
|
-
return `## keynv (secrets)
|
|
7133
|
-
|
|
7134
|
-
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.
|
|
7135
|
-
|
|
7136
|
-
### Mental model
|
|
7137
|
-
|
|
7138
|
-
- **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
|
|
7139
|
-
- **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
|
|
7140
|
-
- **\`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.
|
|
7141
|
-
- **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.
|
|
7142
|
-
|
|
7143
|
-
### What you should do
|
|
7144
|
-
|
|
7145
|
-
When the user asks about secrets or env vars, follow this decision tree:
|
|
7146
|
-
|
|
7147
|
-
| User intent | Run / suggest |
|
|
7148
|
-
|---|---|
|
|
7149
|
-
| "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
|
|
7150
|
-
| "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
|
|
7151
|
-
| "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
|
|
7152
|
-
| "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
|
|
7153
|
-
| "Rotate this key" | \`keynv secret rotate @alias\` |
|
|
7154
|
-
| "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\`) |
|
|
7155
|
-
| "Who has access?" | \`keynv member list <project>\` |
|
|
7156
|
-
|
|
7157
|
-
### Hard rules \u2014 do not violate
|
|
7158
|
-
|
|
7159
|
-
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.
|
|
7160
|
-
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.
|
|
7161
|
-
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.
|
|
7162
|
-
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.
|
|
7163
|
-
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.
|
|
7164
|
-
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.
|
|
7165
|
-
|
|
7166
|
-
### Quick reference
|
|
7167
|
-
|
|
7168
|
-
\`\`\`bash
|
|
7169
|
-
keynv # interactive TUI menu (pick projects, secrets, members)
|
|
7170
|
-
keynv exec -- <cmd> # run cmd with .keynv.env loaded
|
|
7171
|
-
keynv secret create # walk through creating a new secret
|
|
7172
|
-
keynv secret list <project> # list aliases (no values)
|
|
7173
|
-
keynv whoami # who am I logged in as
|
|
7174
|
-
keynv --help # full command list
|
|
7175
|
-
\`\`\`
|
|
7176
|
-
|
|
7177
|
-
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\`.`;
|
|
7178
|
-
}
|
|
7179
|
-
function renderKeynvBlock() {
|
|
7180
|
-
return `${KEYNV_BLOCK_START}
|
|
7181
|
-
${aiContextBody()}
|
|
7182
|
-
${KEYNV_BLOCK_END}`;
|
|
7183
|
-
}
|
|
7184
|
-
function writeAiContext(rootPath) {
|
|
7185
|
-
const path = join(rootPath, AGENTS_FILE_BASENAME);
|
|
7186
|
-
const block = renderKeynvBlock();
|
|
7187
|
-
if (!existsSync(path)) {
|
|
7188
|
-
const initial = `# Agent guidance for this project
|
|
7189
|
-
|
|
7190
|
-
${block}
|
|
7191
|
-
`;
|
|
7192
|
-
writeFileSync(path, initial);
|
|
7193
|
-
return "created";
|
|
7194
|
-
}
|
|
7195
|
-
const existing = readFileSync(path, "utf8");
|
|
7196
|
-
const startIdx = existing.indexOf(KEYNV_BLOCK_START);
|
|
7197
|
-
const endIdx = existing.indexOf(KEYNV_BLOCK_END);
|
|
7198
|
-
if (startIdx >= 0 && endIdx > startIdx) {
|
|
7199
|
-
const before = existing.slice(0, startIdx);
|
|
7200
|
-
const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
|
|
7201
|
-
const next = `${before}${block}${after}`;
|
|
7202
|
-
if (next === existing) return "unchanged";
|
|
7203
|
-
writeFileSync(path, next);
|
|
7204
|
-
return "updated";
|
|
7205
|
-
}
|
|
7206
|
-
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
7207
|
-
const trailingNewline = existing.endsWith("\n") ? "" : "\n";
|
|
7208
|
-
writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
|
|
7209
|
-
`);
|
|
7210
|
-
return "appended";
|
|
7211
|
-
}
|
|
7212
|
-
var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
|
|
7213
|
-
var init_aiContext = __esm({
|
|
7214
|
-
"src/init/aiContext.ts"() {
|
|
7215
|
-
AGENTS_FILE_BASENAME = "AGENTS.md";
|
|
7216
|
-
KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
|
|
7217
|
-
KEYNV_BLOCK_END = "<!-- keynv:end -->";
|
|
7218
|
-
}
|
|
7219
|
-
});
|
|
7220
|
-
|
|
7221
|
-
// src/init/heuristics.ts
|
|
7222
|
-
function shannonEntropyBits(s) {
|
|
7223
|
-
if (s.length === 0) return 0;
|
|
7224
|
-
const counts = /* @__PURE__ */ new Map();
|
|
7225
|
-
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
7226
|
-
let h2 = 0;
|
|
7227
|
-
for (const c2 of counts.values()) {
|
|
7228
|
-
const p2 = c2 / s.length;
|
|
7229
|
-
h2 -= p2 * Math.log2(p2);
|
|
7230
|
-
}
|
|
7231
|
-
return h2;
|
|
7232
|
-
}
|
|
7233
|
-
function nameMatchesLiteralPrefix(name) {
|
|
7234
|
-
const upper = name.toUpperCase();
|
|
7235
|
-
return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
|
|
7236
|
-
}
|
|
7237
|
-
function nameMatchesSecretSuffix(name) {
|
|
7238
|
-
const upper = name.toUpperCase();
|
|
7239
|
-
for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
|
|
7240
|
-
if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
|
|
7241
|
-
return { matched: true, hint };
|
|
7242
|
-
}
|
|
7243
|
-
}
|
|
7244
|
-
return { matched: false, hint: "" };
|
|
7245
|
-
}
|
|
7246
|
-
function classifyEntry(name, value) {
|
|
7247
|
-
const upper = name.toUpperCase();
|
|
7248
|
-
if (NAME_LITERAL_EXACT.has(upper)) {
|
|
7249
|
-
return { verdict: "literal", hint: "common config var" };
|
|
7250
|
-
}
|
|
7251
|
-
if (nameMatchesLiteralPrefix(name)) {
|
|
7252
|
-
return { verdict: "literal", hint: "public env (build-time bundled)" };
|
|
7253
|
-
}
|
|
7254
|
-
if (value.length === 0) {
|
|
7255
|
-
return { verdict: "literal", hint: "empty" };
|
|
7256
|
-
}
|
|
7257
|
-
for (const { re, hint } of VALUE_PATTERNS) {
|
|
7258
|
-
if (re.test(value)) return { verdict: "secret", hint };
|
|
7259
|
-
}
|
|
7260
|
-
const suffix = nameMatchesSecretSuffix(name);
|
|
7261
|
-
if (suffix.matched) {
|
|
7262
|
-
return { verdict: "secret", hint: suffix.hint };
|
|
7263
|
-
}
|
|
7264
|
-
if (NAME_DB_URL.test(name)) {
|
|
7265
|
-
return { verdict: "secret", hint: "database URL" };
|
|
7266
|
-
}
|
|
7267
|
-
if (value.length >= 32) {
|
|
7268
|
-
const bits = shannonEntropyBits(value);
|
|
7269
|
-
if (bits >= 3.5) {
|
|
7270
|
-
return { verdict: "secret", hint: `${value.length}-char random-looking string` };
|
|
7271
|
-
}
|
|
7272
|
-
}
|
|
7273
|
-
return { verdict: "ambiguous", hint: "" };
|
|
7274
|
-
}
|
|
7275
|
-
function previewValue(value, max = 40) {
|
|
7276
|
-
if (value.length === 0) return "(empty)";
|
|
7277
|
-
if (value.length <= max) return value;
|
|
7278
|
-
return `${value.slice(0, max - 1)}\u2026`;
|
|
7279
|
-
}
|
|
7280
|
-
var NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
|
|
7281
|
-
var init_heuristics = __esm({
|
|
7282
|
-
"src/init/heuristics.ts"() {
|
|
7283
|
-
NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
|
|
7284
|
-
"NODE_ENV",
|
|
7285
|
-
"PORT",
|
|
7286
|
-
"HOST",
|
|
7287
|
-
"HOSTNAME",
|
|
7288
|
-
"DEBUG",
|
|
7289
|
-
"LOG_LEVEL",
|
|
7290
|
-
"LOGLEVEL",
|
|
7291
|
-
"TZ",
|
|
7292
|
-
"LANG",
|
|
7293
|
-
"LC_ALL",
|
|
7294
|
-
"PATH",
|
|
7295
|
-
"HOME",
|
|
7296
|
-
"USER",
|
|
7297
|
-
"SHELL",
|
|
7298
|
-
"PWD",
|
|
7299
|
-
"CI",
|
|
7300
|
-
"NODE_OPTIONS",
|
|
7301
|
-
"NPM_CONFIG_LOGLEVEL",
|
|
7302
|
-
"TS_NODE_PROJECT"
|
|
7303
|
-
]);
|
|
7304
|
-
NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
|
|
7305
|
-
NAME_SECRET_SUFFIXES = [
|
|
7306
|
-
{ suffix: "PRIVATE_KEY", hint: "private key" },
|
|
7307
|
-
{ suffix: "API_KEY", hint: "API key" },
|
|
7308
|
-
{ suffix: "ACCESS_KEY", hint: "access key" },
|
|
7309
|
-
{ suffix: "SECRET_KEY", hint: "secret key" },
|
|
7310
|
-
{ suffix: "SECRET", hint: "secret" },
|
|
7311
|
-
{ suffix: "PASSWORD", hint: "password" },
|
|
7312
|
-
{ suffix: "PASSPHRASE", hint: "passphrase" },
|
|
7313
|
-
{ suffix: "TOKEN", hint: "token" },
|
|
7314
|
-
{ suffix: "KEY", hint: "key" },
|
|
7315
|
-
{ suffix: "CREDENTIALS", hint: "credentials" },
|
|
7316
|
-
{ suffix: "AUTH", hint: "auth" },
|
|
7317
|
-
{ suffix: "DSN", hint: "connection string" }
|
|
7318
|
-
];
|
|
7319
|
-
NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
|
|
7320
|
-
VALUE_PATTERNS = [
|
|
7321
|
-
{ re: /^sk-proj-/, hint: "OpenAI project key" },
|
|
7322
|
-
{ re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
|
|
7323
|
-
{ re: /^sk_live_/, hint: "Stripe live key" },
|
|
7324
|
-
{ re: /^sk_test_/, hint: "Stripe test key" },
|
|
7325
|
-
{ re: /^pk_live_/, hint: "Stripe publishable (often public, double-check)" },
|
|
7326
|
-
{ re: /^xoxb-/, hint: "Slack bot token" },
|
|
7327
|
-
{ re: /^xoxp-/, hint: "Slack user token" },
|
|
7328
|
-
{ re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
|
|
7329
|
-
{ re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
|
|
7330
|
-
{ re: /^gho_/, hint: "GitHub OAuth token" },
|
|
7331
|
-
{ re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
|
|
7332
|
-
{ re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
|
|
7333
|
-
{ re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
|
|
7334
|
-
{ re: /^ya29\./, hint: "Google OAuth access token" },
|
|
7335
|
-
{ re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
|
|
7336
|
-
{ re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
|
|
7337
|
-
{ re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
|
|
7338
|
-
];
|
|
7339
|
-
}
|
|
7340
|
-
});
|
|
7341
7407
|
|
|
7342
7408
|
// src/init/scriptWrap.ts
|
|
7343
7409
|
function analyzeScript(name, command) {
|
|
@@ -7571,14 +7637,14 @@ async function runInitFlow(client, opts) {
|
|
|
7571
7637
|
R2.warn(`package.json at ${root.path} is not valid JSON \u2014 script wrapping will be skipped.`);
|
|
7572
7638
|
}
|
|
7573
7639
|
const envFiles = findEnvFiles(root.path);
|
|
7574
|
-
|
|
7640
|
+
const intoExisting = hasExistingKeynvEnv(root.path);
|
|
7641
|
+
if (envFiles.length === 0 && !intoExisting) {
|
|
7575
7642
|
R2.info(
|
|
7576
7643
|
`No .env files found in ${root.path}. There's nothing to migrate yet \u2014 create a .keynv.env by hand or run \`keynv exec\` once you have one.`
|
|
7577
7644
|
);
|
|
7578
7645
|
ye("Nothing to do.");
|
|
7579
7646
|
return { exitCode: 0 };
|
|
7580
7647
|
}
|
|
7581
|
-
const intoExisting = hasExistingKeynvEnv(root.path);
|
|
7582
7648
|
Se(
|
|
7583
7649
|
[
|
|
7584
7650
|
`Project root: ${root.path}`,
|
|
@@ -7588,43 +7654,79 @@ async function runInitFlow(client, opts) {
|
|
|
7588
7654
|
].filter(Boolean).join("\n"),
|
|
7589
7655
|
"Detected"
|
|
7590
7656
|
);
|
|
7591
|
-
const merged = mergeEnvFiles(envFiles);
|
|
7592
|
-
if (merged.length === 0) {
|
|
7593
|
-
R2.info("All env files were empty (only comments/blanks). Nothing to upload.");
|
|
7594
|
-
ye("Done.");
|
|
7595
|
-
return { exitCode: 0 };
|
|
7596
|
-
}
|
|
7597
7657
|
const projectChoice = await pickOrCreateProject(client, root.suggestedName);
|
|
7598
7658
|
if (projectChoice === null) {
|
|
7599
7659
|
me("No project selected.");
|
|
7600
7660
|
return { exitCode: 130 };
|
|
7601
7661
|
}
|
|
7602
|
-
const
|
|
7603
|
-
if (
|
|
7604
|
-
me("No
|
|
7662
|
+
const fileMapping = await pickFileEnvMapping(envFiles, projectChoice);
|
|
7663
|
+
if (fileMapping === null) {
|
|
7664
|
+
me("No env mapping selected.");
|
|
7605
7665
|
return { exitCode: 130 };
|
|
7606
7666
|
}
|
|
7607
|
-
const
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
}
|
|
7622
|
-
|
|
7623
|
-
|
|
7667
|
+
const distinctEnvs = [...new Set(fileMapping.map((m) => m.envName))];
|
|
7668
|
+
const perEnv = parseAndMergePerEnv(fileMapping);
|
|
7669
|
+
const skipped = [];
|
|
7670
|
+
for (const env2 of distinctEnvs) {
|
|
7671
|
+
const before = perEnv.get(env2) ?? [];
|
|
7672
|
+
const kept = [];
|
|
7673
|
+
for (const e2 of before) {
|
|
7674
|
+
if (classifyEntry(e2.name, e2.value).verdict === "skip") {
|
|
7675
|
+
skipped.push({ env: env2, entry: e2 });
|
|
7676
|
+
} else {
|
|
7677
|
+
kept.push(e2);
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
perEnv.set(env2, kept);
|
|
7681
|
+
}
|
|
7682
|
+
if (skipped.length > 0) {
|
|
7683
|
+
const names = [...new Set(skipped.map((s) => s.entry.name))];
|
|
7684
|
+
R2.info(
|
|
7685
|
+
`Skipped ${skipped.length} framework/shell-managed entr${skipped.length === 1 ? "y" : "ies"}: ${names.join(", ")}`
|
|
7686
|
+
);
|
|
7687
|
+
}
|
|
7688
|
+
for (const env2 of distinctEnvs) {
|
|
7689
|
+
const shadowed = (perEnv.get(env2) ?? []).filter((e2) => e2.shadowedBy.length > 0);
|
|
7690
|
+
if (shadowed.length === 0) continue;
|
|
7691
|
+
const detail = shadowed.map((e2) => ` [${env2}] ${e2.name}: ${e2.source} \u2192 ${e2.shadowedBy[e2.shadowedBy.length - 1]}`).join("\n");
|
|
7692
|
+
R2.warn(
|
|
7693
|
+
`Some keys appear in multiple env files mapped to the same env; using the last value (dotenv convention):
|
|
7694
|
+
${detail}`
|
|
7695
|
+
);
|
|
7696
|
+
}
|
|
7697
|
+
const totalEntries = [...perEnv.values()].reduce((n, arr) => n + arr.length, 0);
|
|
7698
|
+
if (totalEntries === 0) {
|
|
7699
|
+
R2.info(
|
|
7700
|
+
"All env files were empty or only contained framework-managed vars. Nothing to upload."
|
|
7701
|
+
);
|
|
7702
|
+
ye("Done.");
|
|
7703
|
+
return { exitCode: 0 };
|
|
7704
|
+
}
|
|
7705
|
+
const choices = [];
|
|
7706
|
+
for (const env2 of distinctEnvs) {
|
|
7707
|
+
for (const e2 of perEnv.get(env2) ?? []) {
|
|
7708
|
+
const c2 = classifyEntry(e2.name, e2.value);
|
|
7709
|
+
const hint = c2.hint || (e2.isAlias ? "looks like an alias literal" : "no signal");
|
|
7710
|
+
const preview2 = e2.isAlias ? e2.value : previewValue(e2.value, 28);
|
|
7711
|
+
const envTag = distinctEnvs.length > 1 ? `[${env2}] ` : "";
|
|
7712
|
+
choices.push({
|
|
7713
|
+
composite: `${env2}|${e2.name}`,
|
|
7714
|
+
env: env2,
|
|
7715
|
+
name: e2.name,
|
|
7716
|
+
value: e2.value,
|
|
7717
|
+
isAlias: e2.isAlias,
|
|
7718
|
+
verdict: c2.verdict,
|
|
7719
|
+
label: `${envTag}${e2.name} ${preview2}`,
|
|
7720
|
+
hint
|
|
7721
|
+
});
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.composite);
|
|
7725
|
+
const selectedComposites = unwrap(
|
|
7624
7726
|
await ve({
|
|
7625
7727
|
message: "Mark which keys are secrets (vault-uploaded). Unchecked keys stay as literals.",
|
|
7626
7728
|
options: choices.map((c2) => ({
|
|
7627
|
-
value: c2.
|
|
7729
|
+
value: c2.composite,
|
|
7628
7730
|
label: c2.label,
|
|
7629
7731
|
hint: c2.isAlias ? `${c2.hint} \u2014 already aliased; will pass through` : c2.hint
|
|
7630
7732
|
})),
|
|
@@ -7632,50 +7734,26 @@ async function runInitFlow(client, opts) {
|
|
|
7632
7734
|
required: false
|
|
7633
7735
|
})
|
|
7634
7736
|
);
|
|
7635
|
-
const selected = new Set(
|
|
7737
|
+
const selected = new Set(selectedComposites);
|
|
7738
|
+
const defaultEnv = pickDefaultEnv(distinctEnvs);
|
|
7636
7739
|
let scriptWrapSelection = [];
|
|
7637
|
-
|
|
7740
|
+
const scriptPlan = root.packageJsonScripts ? planScriptWrap(root.packageJsonScripts) : null;
|
|
7638
7741
|
if (!opts.noScripts && scriptPlan && scriptPlan.recommended.length > 0) {
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
})),
|
|
7648
|
-
...scriptPlan.unknown.map((a) => ({
|
|
7649
|
-
value: a.name,
|
|
7650
|
-
label: `${a.name} \u2192 ${a.wrapped}`,
|
|
7651
|
-
hint: `unknown tool \u2014 opt in if you know it reads env`
|
|
7652
|
-
}))
|
|
7653
|
-
],
|
|
7654
|
-
initialValues: scriptPlan.recommended.map((a) => a.name),
|
|
7655
|
-
required: false
|
|
7656
|
-
})
|
|
7657
|
-
);
|
|
7658
|
-
scriptWrapSelection = wrapAnswer;
|
|
7659
|
-
} else if (opts.noScripts) {
|
|
7660
|
-
scriptPlan = null;
|
|
7661
|
-
}
|
|
7662
|
-
const envFileFateRaw = unwrap(
|
|
7663
|
-
await Ee({
|
|
7664
|
-
message: "After upload, what about the original .env files?",
|
|
7665
|
-
options: [
|
|
7666
|
-
{ value: "delete", label: "Delete (recommended \u2014 eliminates the leak vector)" },
|
|
7667
|
-
{ value: "gitignore", label: "Keep but make sure .gitignore covers them" }
|
|
7668
|
-
],
|
|
7669
|
-
initialValue: "delete"
|
|
7670
|
-
})
|
|
7671
|
-
);
|
|
7742
|
+
scriptWrapSelection = scriptPlan.recommended.map((a) => a.name);
|
|
7743
|
+
}
|
|
7744
|
+
const perEnvCounts = distinctEnvs.map((env2) => {
|
|
7745
|
+
const entries = perEnv.get(env2) ?? [];
|
|
7746
|
+
const sec = entries.filter((e2) => selected.has(`${env2}|${e2.name}`)).length;
|
|
7747
|
+
const lit = entries.length - sec;
|
|
7748
|
+
return ` ${env2}: ${sec} secrets, ${lit} literals${env2 === defaultEnv ? " (default \u2014 written to .keynv.env)" : ""}`;
|
|
7749
|
+
}).join("\n");
|
|
7672
7750
|
const planSummary = [
|
|
7673
|
-
`Project:
|
|
7674
|
-
`
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
`Script wraps:
|
|
7678
|
-
|
|
7751
|
+
`Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
|
|
7752
|
+
`Environments: ${distinctEnvs.join(", ")}`,
|
|
7753
|
+
"Per-env breakdown:",
|
|
7754
|
+
perEnvCounts,
|
|
7755
|
+
`Script wraps: ${scriptWrapSelection.length}`,
|
|
7756
|
+
"Original .env: delete after upload",
|
|
7679
7757
|
opts.dryRun ? "Dry-run: no changes will be made." : ""
|
|
7680
7758
|
].filter(Boolean).join("\n");
|
|
7681
7759
|
Se(planSummary, "About to apply");
|
|
@@ -7688,82 +7766,106 @@ async function runInitFlow(client, opts) {
|
|
|
7688
7766
|
ye("Dry-run complete \u2014 no changes were made.");
|
|
7689
7767
|
return { exitCode: 0 };
|
|
7690
7768
|
}
|
|
7691
|
-
|
|
7692
|
-
if (
|
|
7693
|
-
|
|
7694
|
-
s.start(`Creating project "${projectChoice.name}"`);
|
|
7695
|
-
try {
|
|
7696
|
-
const created = await client.request("/v1/projects", {
|
|
7697
|
-
method: "POST",
|
|
7698
|
-
body: {
|
|
7699
|
-
name: projectChoice.name,
|
|
7700
|
-
environments: [{ name: envName, tier: "non-production", require_approval: false }]
|
|
7701
|
-
}
|
|
7702
|
-
});
|
|
7703
|
-
projectId2 = created.id;
|
|
7704
|
-
s.stop(`Created project ${created.name}`);
|
|
7705
|
-
} catch (err) {
|
|
7706
|
-
s.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
|
|
7707
|
-
return { exitCode: 1 };
|
|
7708
|
-
}
|
|
7709
|
-
} else {
|
|
7710
|
-
projectId2 = projectChoice.id;
|
|
7711
|
-
}
|
|
7712
|
-
const uploaded = [];
|
|
7769
|
+
const projectId2 = await ensureProjectAndEnvs(client, projectChoice, distinctEnvs);
|
|
7770
|
+
if (projectId2 === null) return { exitCode: 1 };
|
|
7771
|
+
const uploadedByEnv = /* @__PURE__ */ new Map();
|
|
7713
7772
|
const failed = [];
|
|
7714
|
-
|
|
7773
|
+
const totalToUpload = selected.size;
|
|
7774
|
+
if (totalToUpload > 0) {
|
|
7715
7775
|
const s = ft();
|
|
7716
|
-
s.start(`Uploading ${
|
|
7776
|
+
s.start(`Uploading ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
|
|
7717
7777
|
let i = 0;
|
|
7718
|
-
for (const
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7778
|
+
for (const env2 of distinctEnvs) {
|
|
7779
|
+
const envUploaded = /* @__PURE__ */ new Map();
|
|
7780
|
+
uploadedByEnv.set(env2, envUploaded);
|
|
7781
|
+
for (const e2 of perEnv.get(env2) ?? []) {
|
|
7782
|
+
if (!selected.has(`${env2}|${e2.name}`)) continue;
|
|
7783
|
+
i++;
|
|
7784
|
+
s.message(`Uploading (${i}/${totalToUpload}) [${env2}] ${e2.name}`);
|
|
7785
|
+
const aliasKey = e2.name.toLowerCase().replace(/_/g, "-");
|
|
7786
|
+
try {
|
|
7787
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
7788
|
+
method: "POST",
|
|
7789
|
+
body: { env: env2, key: aliasKey, value: e2.value }
|
|
7790
|
+
});
|
|
7791
|
+
const alias2 = buildAlias({
|
|
7792
|
+
project: projectChoice.name,
|
|
7793
|
+
environment: env2,
|
|
7794
|
+
key: aliasKey
|
|
7795
|
+
});
|
|
7796
|
+
if (alias2 === null) {
|
|
7797
|
+
failed.push({
|
|
7798
|
+
env: env2,
|
|
7799
|
+
name: e2.name,
|
|
7800
|
+
reason: `produced an invalid alias for project=${projectChoice.name} env=${env2} key=${aliasKey}`
|
|
7801
|
+
});
|
|
7802
|
+
} else {
|
|
7803
|
+
envUploaded.set(e2.name, alias2.literal);
|
|
7804
|
+
}
|
|
7805
|
+
} catch (err) {
|
|
7730
7806
|
failed.push({
|
|
7731
|
-
|
|
7732
|
-
|
|
7807
|
+
env: env2,
|
|
7808
|
+
name: e2.name,
|
|
7809
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
7733
7810
|
});
|
|
7734
|
-
} else {
|
|
7735
|
-
uploaded.push({ name: entry2.name, aliasLiteral: alias2.literal });
|
|
7736
7811
|
}
|
|
7737
|
-
} catch (err) {
|
|
7738
|
-
failed.push({ name: entry2.name, reason: err instanceof Error ? err.message : String(err) });
|
|
7739
7812
|
}
|
|
7740
7813
|
}
|
|
7741
7814
|
if (failed.length === 0) {
|
|
7742
|
-
s.stop(`Uploaded ${
|
|
7815
|
+
s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
|
|
7743
7816
|
} else {
|
|
7744
|
-
s.error(
|
|
7745
|
-
|
|
7817
|
+
s.error(
|
|
7818
|
+
`${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`
|
|
7819
|
+
);
|
|
7820
|
+
for (const f of failed) R2.warn(` [${f.env}] ${f.name}: ${f.reason}`);
|
|
7746
7821
|
}
|
|
7747
7822
|
}
|
|
7748
|
-
const
|
|
7749
|
-
const
|
|
7823
|
+
const defaultUploaded = uploadedByEnv.get(defaultEnv) ?? /* @__PURE__ */ new Map();
|
|
7824
|
+
const defaultLiterals = (perEnv.get(defaultEnv) ?? []).filter(
|
|
7825
|
+
(e2) => !selected.has(`${defaultEnv}|${e2.name}`)
|
|
7826
|
+
);
|
|
7750
7827
|
const keynvEnvPath = join(root.path, ".keynv.env");
|
|
7751
|
-
let writtenLines;
|
|
7752
7828
|
try {
|
|
7753
|
-
|
|
7754
|
-
uploadedAliases:
|
|
7755
|
-
literals,
|
|
7829
|
+
const lines = composeKeynvEnv({
|
|
7830
|
+
uploadedAliases: defaultUploaded,
|
|
7831
|
+
literals: defaultLiterals,
|
|
7756
7832
|
mergeWithExisting: intoExisting ? readFileSync(keynvEnvPath, "utf8") : null
|
|
7757
7833
|
});
|
|
7758
|
-
writeFileSync(keynvEnvPath, `${
|
|
7834
|
+
writeFileSync(keynvEnvPath, `${lines.join("\n")}
|
|
7759
7835
|
`);
|
|
7760
7836
|
R2.success(
|
|
7761
|
-
`${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${
|
|
7837
|
+
`${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${defaultUploaded.size + defaultLiterals.length} entries from "${defaultEnv}")`
|
|
7762
7838
|
);
|
|
7763
7839
|
} catch (err) {
|
|
7764
7840
|
R2.error(`Failed to write .keynv.env: ${err instanceof Error ? err.message : String(err)}`);
|
|
7765
7841
|
return { exitCode: 1 };
|
|
7766
7842
|
}
|
|
7843
|
+
const otherEnvsWithAliases = distinctEnvs.filter(
|
|
7844
|
+
(env2) => env2 !== defaultEnv && (uploadedByEnv.get(env2)?.size ?? 0) > 0
|
|
7845
|
+
);
|
|
7846
|
+
for (const env2 of otherEnvsWithAliases) {
|
|
7847
|
+
const envUploaded = uploadedByEnv.get(env2) ?? /* @__PURE__ */ new Map();
|
|
7848
|
+
const envLiterals = (perEnv.get(env2) ?? []).filter(
|
|
7849
|
+
(e2) => !selected.has(`${env2}|${e2.name}`)
|
|
7850
|
+
);
|
|
7851
|
+
const envFilePath = join(root.path, `.keynv.${env2}.env`);
|
|
7852
|
+
try {
|
|
7853
|
+
const lines = composeKeynvEnv({
|
|
7854
|
+
uploadedAliases: envUploaded,
|
|
7855
|
+
literals: envLiterals,
|
|
7856
|
+
mergeWithExisting: null
|
|
7857
|
+
});
|
|
7858
|
+
writeFileSync(envFilePath, `${lines.join("\n")}
|
|
7859
|
+
`);
|
|
7860
|
+
R2.success(
|
|
7861
|
+
`Wrote .keynv.${env2}.env (${envUploaded.size} secret alias${envUploaded.size === 1 ? "" : "es"} from "${env2}")`
|
|
7862
|
+
);
|
|
7863
|
+
} catch (err) {
|
|
7864
|
+
R2.warn(
|
|
7865
|
+
`Could not write .keynv.${env2}.env: ${err instanceof Error ? err.message : String(err)}`
|
|
7866
|
+
);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7767
7869
|
try {
|
|
7768
7870
|
const outcome = writeAiContext(root.path);
|
|
7769
7871
|
if (outcome === "created") R2.success("Wrote AGENTS.md (so AI agents understand keynv)");
|
|
@@ -7779,66 +7881,37 @@ async function runInitFlow(client, opts) {
|
|
|
7779
7881
|
root.packageJsonScripts,
|
|
7780
7882
|
scriptWrapSelection
|
|
7781
7883
|
);
|
|
7782
|
-
R2.success(
|
|
7884
|
+
R2.success(
|
|
7885
|
+
`Wrapped ${scriptWrapSelection.length} script${scriptWrapSelection.length === 1 ? "" : "s"} in package.json`
|
|
7886
|
+
);
|
|
7783
7887
|
} catch (err) {
|
|
7784
7888
|
R2.warn(
|
|
7785
7889
|
`Could not update package.json scripts: ${err instanceof Error ? err.message : String(err)}`
|
|
7786
7890
|
);
|
|
7787
7891
|
}
|
|
7788
7892
|
}
|
|
7789
|
-
|
|
7790
|
-
for (const f of envFiles) {
|
|
7791
|
-
try {
|
|
7792
|
-
unlinkSync(f.path);
|
|
7793
|
-
R2.success(`Removed ${f.name}`);
|
|
7794
|
-
} catch (err) {
|
|
7795
|
-
R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7796
|
-
}
|
|
7797
|
-
}
|
|
7798
|
-
} else {
|
|
7799
|
-
const gitignorePath = join(root.path, ".gitignore");
|
|
7893
|
+
for (const f of envFiles) {
|
|
7800
7894
|
try {
|
|
7801
|
-
|
|
7802
|
-
R2.success(`
|
|
7895
|
+
unlinkSync(f.path);
|
|
7896
|
+
R2.success(`Removed ${f.name}`);
|
|
7803
7897
|
} catch (err) {
|
|
7804
|
-
R2.warn(`Could not
|
|
7898
|
+
R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7805
7899
|
}
|
|
7806
7900
|
}
|
|
7901
|
+
const otherEnvs = distinctEnvs.filter((e2) => e2 !== defaultEnv);
|
|
7902
|
+
if (otherEnvs.length > 0) {
|
|
7903
|
+
const lines = otherEnvs.map((env2) => {
|
|
7904
|
+
const count = uploadedByEnv.get(env2)?.size ?? 0;
|
|
7905
|
+
const hasFile = count > 0;
|
|
7906
|
+
return hasFile ? ` ${env2}: ${count} secret${count === 1 ? "" : "s"} \u2192 .keynv.${env2}.env (use \`keynv exec --from .keynv.${env2}.env -- <cmd>\`)` : ` ${env2}: 0 secrets in vault (no alias file written)`;
|
|
7907
|
+
});
|
|
7908
|
+
Se(lines.join("\n"), "Secrets in other envs");
|
|
7909
|
+
}
|
|
7807
7910
|
ye(
|
|
7808
|
-
failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "
|
|
7911
|
+
failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "pnpm dev" : "keynv exec -- <your command>"}`
|
|
7809
7912
|
);
|
|
7810
7913
|
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
7811
7914
|
}
|
|
7812
|
-
function mergeEnvFiles(files) {
|
|
7813
|
-
const map = /* @__PURE__ */ new Map();
|
|
7814
|
-
for (const f of files) {
|
|
7815
|
-
let entries;
|
|
7816
|
-
try {
|
|
7817
|
-
entries = parseEnvFile(readFileSync(f.path, "utf8"), f.path);
|
|
7818
|
-
} catch (err) {
|
|
7819
|
-
R2.warn(`${f.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
|
|
7820
|
-
continue;
|
|
7821
|
-
}
|
|
7822
|
-
for (const e2 of entries) {
|
|
7823
|
-
const existing = map.get(e2.name);
|
|
7824
|
-
if (existing) {
|
|
7825
|
-
existing.shadowedBy.push(f.name);
|
|
7826
|
-
existing.value = e2.value;
|
|
7827
|
-
existing.isAlias = e2.isAlias;
|
|
7828
|
-
} else {
|
|
7829
|
-
map.set(e2.name, {
|
|
7830
|
-
name: e2.name,
|
|
7831
|
-
value: e2.value,
|
|
7832
|
-
isAlias: e2.isAlias,
|
|
7833
|
-
source: f.name,
|
|
7834
|
-
sourceLine: e2.line,
|
|
7835
|
-
shadowedBy: []
|
|
7836
|
-
});
|
|
7837
|
-
}
|
|
7838
|
-
}
|
|
7839
|
-
}
|
|
7840
|
-
return [...map.values()];
|
|
7841
|
-
}
|
|
7842
7915
|
async function pickOrCreateProject(client, suggestedName) {
|
|
7843
7916
|
const projects = await listProjects(client);
|
|
7844
7917
|
const value = unwrap(
|
|
@@ -7858,53 +7931,156 @@ async function pickOrCreateProject(client, suggestedName) {
|
|
|
7858
7931
|
validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,47}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 48 chars"
|
|
7859
7932
|
})
|
|
7860
7933
|
);
|
|
7861
|
-
return { id: "", name, created: true };
|
|
7934
|
+
return { id: "", name, created: true, existingEnvs: [] };
|
|
7862
7935
|
}
|
|
7863
7936
|
const match = projects.find((p2) => p2.id === value);
|
|
7864
7937
|
if (!match) return null;
|
|
7865
|
-
|
|
7938
|
+
const detail = await client.request(`/v1/projects/${match.id}`);
|
|
7939
|
+
return {
|
|
7940
|
+
id: match.id,
|
|
7941
|
+
name: match.name,
|
|
7942
|
+
created: false,
|
|
7943
|
+
existingEnvs: detail.environments
|
|
7944
|
+
};
|
|
7866
7945
|
}
|
|
7867
|
-
async function
|
|
7868
|
-
|
|
7869
|
-
const
|
|
7870
|
-
if (
|
|
7871
|
-
const
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7946
|
+
async function pickFileEnvMapping(files, project) {
|
|
7947
|
+
if (files.length === 0) return [];
|
|
7948
|
+
const existingEnvNames = new Set(project.existingEnvs.map((e2) => e2.name));
|
|
7949
|
+
if (files.length === 1) {
|
|
7950
|
+
const onlyFile = files[0];
|
|
7951
|
+
if (!onlyFile) return [];
|
|
7952
|
+
const suggested = suggestedEnvForSuffix(onlyFile.suffix);
|
|
7953
|
+
return [{ file: onlyFile, envName: suggested }];
|
|
7954
|
+
}
|
|
7955
|
+
const assignments = [];
|
|
7956
|
+
for (const f of files) {
|
|
7957
|
+
const suggested = suggestedEnvForSuffix(f.suffix);
|
|
7958
|
+
const opts = [];
|
|
7959
|
+
opts.push({
|
|
7960
|
+
value: suggested,
|
|
7961
|
+
label: suggested,
|
|
7962
|
+
hint: existingEnvNames.has(suggested) ? "existing env" : project.created ? "will be created with project" : "will be added to project"
|
|
7963
|
+
});
|
|
7964
|
+
for (const e2 of project.existingEnvs) {
|
|
7965
|
+
if (e2.name === suggested) continue;
|
|
7966
|
+
opts.push({ value: e2.name, label: e2.name, hint: e2.tier });
|
|
7967
|
+
}
|
|
7968
|
+
opts.push({ value: "__custom", label: "+ Custom env name\u2026" });
|
|
7969
|
+
let envName = unwrap(
|
|
7970
|
+
await Ee({
|
|
7971
|
+
message: `Map ${f.name} to which keynv env?`,
|
|
7972
|
+
options: opts,
|
|
7973
|
+
initialValue: suggested
|
|
7876
7974
|
})
|
|
7877
7975
|
);
|
|
7878
|
-
|
|
7976
|
+
if (envName === "__custom") {
|
|
7977
|
+
envName = unwrap(
|
|
7978
|
+
await Re({
|
|
7979
|
+
message: "New env name",
|
|
7980
|
+
validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,23}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 24 chars"
|
|
7981
|
+
})
|
|
7982
|
+
);
|
|
7983
|
+
}
|
|
7984
|
+
assignments.push({ file: f, envName });
|
|
7879
7985
|
}
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7986
|
+
return assignments;
|
|
7987
|
+
}
|
|
7988
|
+
function parseAndMergePerEnv(mapping) {
|
|
7989
|
+
const acc = /* @__PURE__ */ new Map();
|
|
7990
|
+
for (const { file, envName } of mapping) {
|
|
7991
|
+
let entries;
|
|
7992
|
+
try {
|
|
7993
|
+
entries = parseEnvFile(readFileSync(file.path, "utf8"), file.path);
|
|
7994
|
+
} catch (err) {
|
|
7995
|
+
R2.warn(`${file.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
|
|
7996
|
+
continue;
|
|
7997
|
+
}
|
|
7998
|
+
let envMap = acc.get(envName);
|
|
7999
|
+
if (!envMap) {
|
|
8000
|
+
envMap = /* @__PURE__ */ new Map();
|
|
8001
|
+
acc.set(envName, envMap);
|
|
8002
|
+
}
|
|
8003
|
+
for (const e2 of entries) {
|
|
8004
|
+
const existing = envMap.get(e2.name);
|
|
8005
|
+
if (existing) {
|
|
8006
|
+
existing.shadowedBy.push(file.name);
|
|
8007
|
+
existing.value = e2.value;
|
|
8008
|
+
existing.isAlias = e2.isAlias;
|
|
8009
|
+
} else {
|
|
8010
|
+
envMap.set(e2.name, {
|
|
8011
|
+
name: e2.name,
|
|
8012
|
+
value: e2.value,
|
|
8013
|
+
isAlias: e2.isAlias,
|
|
8014
|
+
source: file.name,
|
|
8015
|
+
sourceLine: e2.line,
|
|
8016
|
+
shadowedBy: []
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
7886
8020
|
}
|
|
7887
|
-
|
|
7888
|
-
|
|
8021
|
+
const out = /* @__PURE__ */ new Map();
|
|
8022
|
+
for (const [env2, m] of acc) out.set(env2, [...m.values()]);
|
|
8023
|
+
return out;
|
|
8024
|
+
}
|
|
8025
|
+
function pickDefaultEnv(envs) {
|
|
8026
|
+
if (envs.length === 0) return "dev";
|
|
8027
|
+
if (envs.includes("dev")) return "dev";
|
|
8028
|
+
return envs[0];
|
|
8029
|
+
}
|
|
8030
|
+
async function ensureProjectAndEnvs(client, projectChoice, distinctEnvs) {
|
|
8031
|
+
if (projectChoice.created) {
|
|
8032
|
+
const s2 = ft();
|
|
8033
|
+
s2.start(`Creating project "${projectChoice.name}" with ${distinctEnvs.length} env(s)`);
|
|
8034
|
+
try {
|
|
8035
|
+
const created = await client.request("/v1/projects", {
|
|
8036
|
+
method: "POST",
|
|
8037
|
+
body: {
|
|
8038
|
+
name: projectChoice.name,
|
|
8039
|
+
environments: distinctEnvs.map((name) => envBodyFor(name))
|
|
8040
|
+
}
|
|
8041
|
+
});
|
|
8042
|
+
s2.stop(`Created project ${created.name}`);
|
|
8043
|
+
return created.id;
|
|
8044
|
+
} catch (err) {
|
|
8045
|
+
s2.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
|
|
8046
|
+
return null;
|
|
8047
|
+
}
|
|
7889
8048
|
}
|
|
7890
|
-
const
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
}
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
8049
|
+
const existing = new Set(projectChoice.existingEnvs.map((e2) => e2.name));
|
|
8050
|
+
const missing = distinctEnvs.filter((e2) => !existing.has(e2));
|
|
8051
|
+
if (missing.length === 0) return projectChoice.id;
|
|
8052
|
+
const s = ft();
|
|
8053
|
+
s.start(`Adding ${missing.length} missing env(s) to project "${projectChoice.name}"`);
|
|
8054
|
+
for (const envName of missing) {
|
|
8055
|
+
try {
|
|
8056
|
+
await client.request(`/v1/projects/${projectChoice.id}/environments`, {
|
|
8057
|
+
method: "POST",
|
|
8058
|
+
body: envBodyFor(envName)
|
|
8059
|
+
});
|
|
8060
|
+
} catch (err) {
|
|
8061
|
+
s.error(
|
|
8062
|
+
`Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`
|
|
8063
|
+
);
|
|
8064
|
+
return null;
|
|
8065
|
+
}
|
|
8066
|
+
}
|
|
8067
|
+
s.stop(`Added env(s): ${missing.join(", ")}`);
|
|
8068
|
+
return projectChoice.id;
|
|
8069
|
+
}
|
|
8070
|
+
function envBodyFor(name) {
|
|
8071
|
+
const isProd = name === "prod" || name === "production";
|
|
8072
|
+
return {
|
|
8073
|
+
name,
|
|
8074
|
+
tier: isProd ? "production" : "non-production",
|
|
8075
|
+
require_approval: isProd
|
|
8076
|
+
};
|
|
7902
8077
|
}
|
|
7903
8078
|
function composeKeynvEnv(opts) {
|
|
7904
8079
|
const { uploadedAliases, literals, mergeWithExisting } = opts;
|
|
7905
8080
|
const lines = [];
|
|
7906
8081
|
if (mergeWithExisting !== null) {
|
|
7907
|
-
const
|
|
8082
|
+
const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
8083
|
+
const trimmed = normalized.replace(/\n+$/, "");
|
|
7908
8084
|
if (trimmed.length > 0) {
|
|
7909
8085
|
lines.push(...trimmed.split("\n"));
|
|
7910
8086
|
lines.push("");
|
|
@@ -7931,7 +8107,7 @@ function composeKeynvEnv(opts) {
|
|
|
7931
8107
|
}
|
|
7932
8108
|
}
|
|
7933
8109
|
if (mergeWithExisting !== null) {
|
|
7934
|
-
lines.push(
|
|
8110
|
+
lines.push("# <<< keynv init <<<");
|
|
7935
8111
|
}
|
|
7936
8112
|
return lines;
|
|
7937
8113
|
}
|
|
@@ -7950,23 +8126,13 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
|
|
|
7950
8126
|
writeFileSync(path, trailingNewline ? `${out}
|
|
7951
8127
|
` : out);
|
|
7952
8128
|
}
|
|
7953
|
-
function ensureGitignoreEntries(path, basenames) {
|
|
7954
|
-
let existing = "";
|
|
7955
|
-
if (existsSync(path)) existing = readFileSync(path, "utf8");
|
|
7956
|
-
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
7957
|
-
const toAdd = basenames.filter((n) => !existingLines.has(n));
|
|
7958
|
-
if (toAdd.length === 0) return;
|
|
7959
|
-
const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
7960
|
-
const block = ["", "# added by keynv init", ...toAdd, ""].join("\n");
|
|
7961
|
-
writeFileSync(path, `${existing}${sep}${block}`);
|
|
7962
|
-
}
|
|
7963
8129
|
var init_init = __esm({
|
|
7964
8130
|
"src/ui/flows/init.ts"() {
|
|
7965
|
-
init_dist();
|
|
7966
8131
|
init_dist5();
|
|
8132
|
+
init_dist();
|
|
7967
8133
|
init_envFile();
|
|
7968
|
-
init_detect();
|
|
7969
8134
|
init_aiContext();
|
|
8135
|
+
init_detect();
|
|
7970
8136
|
init_heuristics();
|
|
7971
8137
|
init_scriptWrap();
|
|
7972
8138
|
init_cancel();
|
|
@@ -7974,30 +8140,122 @@ var init_init = __esm({
|
|
|
7974
8140
|
}
|
|
7975
8141
|
});
|
|
7976
8142
|
|
|
7977
|
-
// src/ui/helpers/
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
8143
|
+
// src/ui/helpers/tty.ts
|
|
8144
|
+
var tty_exports = {};
|
|
8145
|
+
__export(tty_exports, {
|
|
8146
|
+
isInteractive: () => isInteractive
|
|
8147
|
+
});
|
|
8148
|
+
function isInteractive() {
|
|
8149
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
7983
8150
|
}
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
8151
|
+
var init_tty = __esm({
|
|
8152
|
+
"src/ui/helpers/tty.ts"() {
|
|
8153
|
+
}
|
|
8154
|
+
});
|
|
8155
|
+
function sleep(ms) {
|
|
8156
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
8157
|
+
}
|
|
8158
|
+
async function errorMessage(res, fallback) {
|
|
8159
|
+
const text = await res.text();
|
|
8160
|
+
if (!text) return fallback;
|
|
8161
|
+
try {
|
|
8162
|
+
const parsed = JSON.parse(text);
|
|
8163
|
+
return parsed.error?.message ?? fallback;
|
|
8164
|
+
} catch {
|
|
8165
|
+
return fallback;
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
function openBrowser(url) {
|
|
8169
|
+
try {
|
|
8170
|
+
const os = platform();
|
|
8171
|
+
const command = os === "win32" ? "cmd" : os === "darwin" ? "open" : "xdg-open";
|
|
8172
|
+
const args = os === "win32" ? ["/c", "start", "", url] : [url];
|
|
8173
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
8174
|
+
child.unref();
|
|
8175
|
+
return true;
|
|
8176
|
+
} catch {
|
|
8177
|
+
return false;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
async function runBrowserAuth(serverUrl) {
|
|
8181
|
+
const startRes = await fetch(new URL("/v1/auth/cli/browser/start", serverUrl).toString(), {
|
|
8182
|
+
method: "POST",
|
|
8183
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
8184
|
+
body: JSON.stringify({ device_name: hostname() })
|
|
7994
8185
|
});
|
|
7995
|
-
if (
|
|
7996
|
-
|
|
8186
|
+
if (!startRes.ok) {
|
|
8187
|
+
throw new BrowserAuthError(
|
|
8188
|
+
await errorMessage(startRes, `Browser auth failed to start (${startRes.status}).`)
|
|
8189
|
+
);
|
|
8190
|
+
}
|
|
8191
|
+
const start = await startRes.json();
|
|
8192
|
+
const opened = openBrowser(start.verification_uri_complete);
|
|
8193
|
+
if (opened) {
|
|
8194
|
+
process.stderr.write(
|
|
8195
|
+
`
|
|
8196
|
+
Your code: ${start.user_code}
|
|
8197
|
+
Complete auth in your browser, then return here.
|
|
8198
|
+
|
|
8199
|
+
`
|
|
8200
|
+
);
|
|
8201
|
+
} else {
|
|
8202
|
+
process.stderr.write(
|
|
8203
|
+
`
|
|
8204
|
+
Could not open a browser automatically.
|
|
8205
|
+
Open this URL manually:
|
|
8206
|
+
|
|
8207
|
+
${start.verification_uri_complete}
|
|
8208
|
+
|
|
8209
|
+
Your code: ${start.user_code}
|
|
8210
|
+
Waiting for you to complete auth in the browser...
|
|
8211
|
+
|
|
8212
|
+
`
|
|
8213
|
+
);
|
|
8214
|
+
}
|
|
8215
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
8216
|
+
const intervalMs = Math.max(1, start.interval) * 1e3;
|
|
8217
|
+
while (Date.now() < deadline) {
|
|
8218
|
+
await sleep(intervalMs);
|
|
8219
|
+
const pollRes = await fetch(new URL("/v1/auth/cli/browser/poll", serverUrl).toString(), {
|
|
8220
|
+
method: "POST",
|
|
8221
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
8222
|
+
body: JSON.stringify({ device_code: start.device_code })
|
|
8223
|
+
});
|
|
8224
|
+
if (pollRes.status === 202) continue;
|
|
8225
|
+
if (!pollRes.ok) {
|
|
8226
|
+
throw new BrowserAuthError(
|
|
8227
|
+
await errorMessage(pollRes, `Browser auth failed (${pollRes.status}).`)
|
|
8228
|
+
);
|
|
8229
|
+
}
|
|
8230
|
+
const data = await pollRes.json();
|
|
8231
|
+
return {
|
|
8232
|
+
auth_kind: "session",
|
|
8233
|
+
server_url: serverUrl,
|
|
8234
|
+
user_id: data.user.id,
|
|
8235
|
+
email: data.user.email,
|
|
8236
|
+
org_id: data.user.org_id,
|
|
8237
|
+
org_role: data.user.org_role,
|
|
8238
|
+
access_token: data.access_token,
|
|
8239
|
+
refresh_token: data.refresh_token,
|
|
8240
|
+
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
8241
|
+
};
|
|
8242
|
+
}
|
|
8243
|
+
throw new BrowserAuthError("Browser auth timed out. Run `keynv login` to try again.");
|
|
7997
8244
|
}
|
|
7998
|
-
var
|
|
7999
|
-
|
|
8000
|
-
|
|
8245
|
+
var BrowserAuthError;
|
|
8246
|
+
var init_browser_auth = __esm({
|
|
8247
|
+
"src/client/browser-auth.ts"() {
|
|
8248
|
+
init_version();
|
|
8249
|
+
BrowserAuthError = class extends Error {
|
|
8250
|
+
};
|
|
8251
|
+
}
|
|
8252
|
+
});
|
|
8253
|
+
|
|
8254
|
+
// src/client/defaults.ts
|
|
8255
|
+
var DEFAULT_SERVER_URL;
|
|
8256
|
+
var init_defaults = __esm({
|
|
8257
|
+
"src/client/defaults.ts"() {
|
|
8258
|
+
DEFAULT_SERVER_URL = "https://api.keynv.dev";
|
|
8001
8259
|
}
|
|
8002
8260
|
});
|
|
8003
8261
|
|
|
@@ -8025,6 +8283,33 @@ var init_pickEnv = __esm({
|
|
|
8025
8283
|
init_dist5();
|
|
8026
8284
|
}
|
|
8027
8285
|
});
|
|
8286
|
+
|
|
8287
|
+
// src/ui/helpers/pickSecret.ts
|
|
8288
|
+
async function listSecrets(client, projectId2) {
|
|
8289
|
+
const data = await client.request(
|
|
8290
|
+
`/v1/projects/${projectId2}/secrets`
|
|
8291
|
+
);
|
|
8292
|
+
return data.secrets;
|
|
8293
|
+
}
|
|
8294
|
+
async function pickSecret(client, projectId2, message = "Secret:") {
|
|
8295
|
+
const secrets = await listSecrets(client, projectId2);
|
|
8296
|
+
if (secrets.length === 0) return null;
|
|
8297
|
+
const value = await Ee({
|
|
8298
|
+
message,
|
|
8299
|
+
options: secrets.map((s) => ({
|
|
8300
|
+
value: s.alias,
|
|
8301
|
+
label: s.alias,
|
|
8302
|
+
hint: `v${s.version}`
|
|
8303
|
+
}))
|
|
8304
|
+
});
|
|
8305
|
+
if (q(value)) return null;
|
|
8306
|
+
return secrets.find((s) => s.alias === value) ?? null;
|
|
8307
|
+
}
|
|
8308
|
+
var init_pickSecret = __esm({
|
|
8309
|
+
"src/ui/helpers/pickSecret.ts"() {
|
|
8310
|
+
init_dist5();
|
|
8311
|
+
}
|
|
8312
|
+
});
|
|
8028
8313
|
async function runSecretsFlow(client, project) {
|
|
8029
8314
|
let target = project;
|
|
8030
8315
|
if (!target) {
|
|
@@ -8088,7 +8373,10 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8088
8373
|
}
|
|
8089
8374
|
} else if (choice === "rotate") {
|
|
8090
8375
|
const newValue = unwrap(
|
|
8091
|
-
await Ce({
|
|
8376
|
+
await Ce({
|
|
8377
|
+
message: "New value",
|
|
8378
|
+
validate: (v2) => v2?.length ? void 0 : "required"
|
|
8379
|
+
})
|
|
8092
8380
|
);
|
|
8093
8381
|
const data = await client.request(`${path}/rotate`, {
|
|
8094
8382
|
method: "POST",
|
|
@@ -8096,9 +8384,7 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8096
8384
|
});
|
|
8097
8385
|
R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
|
|
8098
8386
|
} else if (choice === "delete") {
|
|
8099
|
-
const confirmed = unwrap(
|
|
8100
|
-
await ue({ message: `Delete ${alias2}?`, initialValue: false })
|
|
8101
|
-
);
|
|
8387
|
+
const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
|
|
8102
8388
|
if (!confirmed) continue;
|
|
8103
8389
|
await client.request(path, { method: "DELETE" });
|
|
8104
8390
|
R2.success(`Deleted ${alias2}`);
|
|
@@ -8107,11 +8393,13 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8107
8393
|
}
|
|
8108
8394
|
}
|
|
8109
8395
|
async function copyToClipboard(value) {
|
|
8110
|
-
const
|
|
8111
|
-
const cmd =
|
|
8396
|
+
const platform2 = process.platform;
|
|
8397
|
+
const cmd = platform2 === "darwin" ? ["pbcopy", []] : platform2 === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
|
|
8112
8398
|
return new Promise((resolve3) => {
|
|
8113
8399
|
try {
|
|
8114
|
-
const child = spawn(cmd[0], cmd[1], {
|
|
8400
|
+
const child = spawn(cmd[0], cmd[1], {
|
|
8401
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
8402
|
+
});
|
|
8115
8403
|
child.on("error", () => resolve3(false));
|
|
8116
8404
|
child.on("exit", (code) => resolve3(code === 0));
|
|
8117
8405
|
child.stdin.end(value);
|
|
@@ -8138,7 +8426,7 @@ async function promptNewSecret(client, project) {
|
|
|
8138
8426
|
}),
|
|
8139
8427
|
value: () => Ce({
|
|
8140
8428
|
message: "Value (hidden)",
|
|
8141
|
-
validate: (v2) => v2
|
|
8429
|
+
validate: (v2) => v2?.length ? void 0 : "required"
|
|
8142
8430
|
})
|
|
8143
8431
|
},
|
|
8144
8432
|
{
|
|
@@ -8167,8 +8455,8 @@ async function createSecretInteractive(client, project) {
|
|
|
8167
8455
|
}
|
|
8168
8456
|
var init_secret = __esm({
|
|
8169
8457
|
"src/ui/flows/secret.ts"() {
|
|
8170
|
-
init_dist();
|
|
8171
8458
|
init_dist5();
|
|
8459
|
+
init_dist();
|
|
8172
8460
|
init_cancel();
|
|
8173
8461
|
init_pickEnv();
|
|
8174
8462
|
init_pickProject();
|
|
@@ -28501,7 +28789,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
28501
28789
|
var nodeCrypto = __require("crypto");
|
|
28502
28790
|
module.exports = {
|
|
28503
28791
|
postgresMd5PasswordHash,
|
|
28504
|
-
randomBytes,
|
|
28792
|
+
randomBytes: randomBytes2,
|
|
28505
28793
|
deriveKey,
|
|
28506
28794
|
sha256,
|
|
28507
28795
|
hashByName,
|
|
@@ -28511,7 +28799,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
28511
28799
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
28512
28800
|
var subtleCrypto = webCrypto.subtle;
|
|
28513
28801
|
var textEncoder = new TextEncoder();
|
|
28514
|
-
function
|
|
28802
|
+
function randomBytes2(length) {
|
|
28515
28803
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
28516
28804
|
}
|
|
28517
28805
|
async function md5(string) {
|
|
@@ -28907,11 +29195,11 @@ var require_pg_connection_string = __commonJS({
|
|
|
28907
29195
|
config.client_encoding = result.searchParams.get("encoding");
|
|
28908
29196
|
return config;
|
|
28909
29197
|
}
|
|
28910
|
-
const
|
|
29198
|
+
const hostname2 = dummyHost ? "" : result.hostname;
|
|
28911
29199
|
if (!config.host) {
|
|
28912
|
-
config.host = decodeURIComponent(
|
|
28913
|
-
} else if (
|
|
28914
|
-
result.pathname =
|
|
29200
|
+
config.host = decodeURIComponent(hostname2);
|
|
29201
|
+
} else if (hostname2 && /^%2f/i.test(hostname2)) {
|
|
29202
|
+
result.pathname = hostname2 + result.pathname;
|
|
28915
29203
|
}
|
|
28916
29204
|
if (!config.port) {
|
|
28917
29205
|
config.port = result.port;
|
|
@@ -40017,9 +40305,9 @@ var require_cluster = __commonJS({
|
|
|
40017
40305
|
}
|
|
40018
40306
|
});
|
|
40019
40307
|
}
|
|
40020
|
-
resolveSrv(
|
|
40308
|
+
resolveSrv(hostname2) {
|
|
40021
40309
|
return new Promise((resolve3, reject) => {
|
|
40022
|
-
this.options.resolveSrv(
|
|
40310
|
+
this.options.resolveSrv(hostname2, (err, records) => {
|
|
40023
40311
|
if (err) {
|
|
40024
40312
|
return reject(err);
|
|
40025
40313
|
}
|
|
@@ -40041,14 +40329,14 @@ var require_cluster = __commonJS({
|
|
|
40041
40329
|
});
|
|
40042
40330
|
});
|
|
40043
40331
|
}
|
|
40044
|
-
dnsLookup(
|
|
40332
|
+
dnsLookup(hostname2) {
|
|
40045
40333
|
return new Promise((resolve3, reject) => {
|
|
40046
|
-
this.options.dnsLookup(
|
|
40334
|
+
this.options.dnsLookup(hostname2, (err, address) => {
|
|
40047
40335
|
if (err) {
|
|
40048
|
-
debug2("failed to resolve hostname %s to IP: %s",
|
|
40336
|
+
debug2("failed to resolve hostname %s to IP: %s", hostname2, err.message);
|
|
40049
40337
|
reject(err);
|
|
40050
40338
|
} else {
|
|
40051
|
-
debug2("resolved hostname %s to IP %s",
|
|
40339
|
+
debug2("resolved hostname %s to IP %s", hostname2, address);
|
|
40052
40340
|
resolve3(address);
|
|
40053
40341
|
}
|
|
40054
40342
|
});
|
|
@@ -42373,85 +42661,27 @@ var init_audit2 = __esm({
|
|
|
42373
42661
|
});
|
|
42374
42662
|
|
|
42375
42663
|
// src/ui/flows/login.ts
|
|
42376
|
-
async function runLoginFlow(client) {
|
|
42377
|
-
const
|
|
42378
|
-
{
|
|
42379
|
-
server: () => Re({
|
|
42380
|
-
message: "Server URL",
|
|
42381
|
-
placeholder: "http://localhost:8080",
|
|
42382
|
-
defaultValue: "http://localhost:8080"
|
|
42383
|
-
}),
|
|
42384
|
-
email: () => Re({
|
|
42385
|
-
message: "Email",
|
|
42386
|
-
validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
|
|
42387
|
-
}),
|
|
42388
|
-
password: () => Ce({
|
|
42389
|
-
message: "Password",
|
|
42390
|
-
validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
|
|
42391
|
-
})
|
|
42392
|
-
},
|
|
42393
|
-
{
|
|
42394
|
-
onCancel: () => {
|
|
42395
|
-
throw new UserCancelled();
|
|
42396
|
-
}
|
|
42397
|
-
}
|
|
42398
|
-
);
|
|
42399
|
-
const server = answers.server || "http://localhost:8080";
|
|
42664
|
+
async function runLoginFlow(client, options = {}) {
|
|
42665
|
+
const server = options.server ?? DEFAULT_SERVER_URL;
|
|
42400
42666
|
const s = ft();
|
|
42401
|
-
s.start("
|
|
42402
|
-
let
|
|
42667
|
+
s.start("Waiting for authorization");
|
|
42668
|
+
let creds;
|
|
42403
42669
|
try {
|
|
42404
|
-
|
|
42405
|
-
method: "POST",
|
|
42406
|
-
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
42407
|
-
body: JSON.stringify({ email: answers.email, password: answers.password })
|
|
42408
|
-
});
|
|
42670
|
+
creds = await runBrowserAuth(server);
|
|
42409
42671
|
} catch (err) {
|
|
42410
|
-
s.error("
|
|
42411
|
-
me(err instanceof Error ? err.message : "
|
|
42672
|
+
s.error("Browser login failed");
|
|
42673
|
+
me(err instanceof Error ? err.message : "Unable to authenticate.");
|
|
42412
42674
|
return false;
|
|
42413
42675
|
}
|
|
42414
|
-
|
|
42415
|
-
|
|
42416
|
-
try {
|
|
42417
|
-
const parsed = await res.json();
|
|
42418
|
-
msg = parsed.error?.message ?? msg;
|
|
42419
|
-
} catch {
|
|
42420
|
-
}
|
|
42421
|
-
s.error(msg);
|
|
42422
|
-
return false;
|
|
42423
|
-
}
|
|
42424
|
-
const data = await res.json();
|
|
42425
|
-
await saveCredentials({
|
|
42426
|
-
server_url: server,
|
|
42427
|
-
user_id: data.user.id,
|
|
42428
|
-
email: data.user.email,
|
|
42429
|
-
org_id: data.user.org_id,
|
|
42430
|
-
org_role: data.user.org_role,
|
|
42431
|
-
access_token: data.access_token,
|
|
42432
|
-
refresh_token: data.refresh_token,
|
|
42433
|
-
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
42434
|
-
});
|
|
42435
|
-
await client.setCredentials({
|
|
42436
|
-
server_url: server,
|
|
42437
|
-
user_id: data.user.id,
|
|
42438
|
-
email: data.user.email,
|
|
42439
|
-
org_id: data.user.org_id,
|
|
42440
|
-
org_role: data.user.org_role,
|
|
42441
|
-
access_token: data.access_token,
|
|
42442
|
-
refresh_token: data.refresh_token,
|
|
42443
|
-
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
42444
|
-
});
|
|
42445
|
-
s.stop(`Logged in as ${data.user.email}`);
|
|
42676
|
+
await client.setCredentials(creds);
|
|
42677
|
+
s.stop(`Logged in as ${creds.email}`);
|
|
42446
42678
|
return true;
|
|
42447
42679
|
}
|
|
42448
42680
|
var init_login = __esm({
|
|
42449
42681
|
"src/ui/flows/login.ts"() {
|
|
42450
42682
|
init_dist5();
|
|
42451
|
-
|
|
42452
|
-
|
|
42453
|
-
init_version();
|
|
42454
|
-
init_cancel();
|
|
42683
|
+
init_browser_auth();
|
|
42684
|
+
init_defaults();
|
|
42455
42685
|
}
|
|
42456
42686
|
});
|
|
42457
42687
|
|
|
@@ -42510,7 +42740,7 @@ async function addMemberInteractive(client, project) {
|
|
|
42510
42740
|
{
|
|
42511
42741
|
email: () => Re({
|
|
42512
42742
|
message: "Email",
|
|
42513
|
-
validate: (v2) => v2
|
|
42743
|
+
validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
|
|
42514
42744
|
}),
|
|
42515
42745
|
role: () => Ee({
|
|
42516
42746
|
message: "Role",
|
|
@@ -42609,8 +42839,11 @@ function printDetail(detail) {
|
|
|
42609
42839
|
const envLines = detail.environments.map(
|
|
42610
42840
|
(e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
|
|
42611
42841
|
);
|
|
42612
|
-
Se(
|
|
42613
|
-
|
|
42842
|
+
Se(
|
|
42843
|
+
`${detail.name} (${detail.id})
|
|
42844
|
+
${envLines.join("\n") || " (no environments)"}`,
|
|
42845
|
+
"Project"
|
|
42846
|
+
);
|
|
42614
42847
|
}
|
|
42615
42848
|
async function createProjectInteractive(client) {
|
|
42616
42849
|
const answers = await he(
|
|
@@ -42669,6 +42902,7 @@ async function runMenu() {
|
|
|
42669
42902
|
ge(`keynv ${VERSION}`);
|
|
42670
42903
|
const client = new ApiClient();
|
|
42671
42904
|
await client.ensureHydrated();
|
|
42905
|
+
let didLogin = false;
|
|
42672
42906
|
if (!client.isLoggedIn) {
|
|
42673
42907
|
R2.info("Not logged in.");
|
|
42674
42908
|
try {
|
|
@@ -42677,6 +42911,7 @@ async function runMenu() {
|
|
|
42677
42911
|
ye("Login cancelled.");
|
|
42678
42912
|
return 1;
|
|
42679
42913
|
}
|
|
42914
|
+
didLogin = true;
|
|
42680
42915
|
} catch (err) {
|
|
42681
42916
|
if (err instanceof UserCancelled) {
|
|
42682
42917
|
me("Login cancelled.");
|
|
@@ -42688,6 +42923,20 @@ async function runMenu() {
|
|
|
42688
42923
|
const u = client.currentUser;
|
|
42689
42924
|
if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
|
|
42690
42925
|
}
|
|
42926
|
+
if (didLogin) {
|
|
42927
|
+
const root = findProjectRoot(process.cwd());
|
|
42928
|
+
const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
|
|
42929
|
+
if (!alreadyInitialized) {
|
|
42930
|
+
const setup = await ue({
|
|
42931
|
+
message: "Set up this project now?",
|
|
42932
|
+
initialValue: true
|
|
42933
|
+
});
|
|
42934
|
+
if (!q(setup) && setup) {
|
|
42935
|
+
const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
42936
|
+
await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
|
|
42937
|
+
}
|
|
42938
|
+
}
|
|
42939
|
+
}
|
|
42691
42940
|
while (true) {
|
|
42692
42941
|
let choice;
|
|
42693
42942
|
try {
|
|
@@ -42754,15 +43003,16 @@ var init_menu = __esm({
|
|
|
42754
43003
|
"src/ui/menu.ts"() {
|
|
42755
43004
|
init_dist5();
|
|
42756
43005
|
init_http();
|
|
43006
|
+
init_detect();
|
|
42757
43007
|
init_store();
|
|
42758
|
-
|
|
42759
|
-
init_cancel();
|
|
43008
|
+
init_version();
|
|
42760
43009
|
init_audit2();
|
|
42761
43010
|
init_login();
|
|
42762
43011
|
init_member();
|
|
42763
43012
|
init_project();
|
|
42764
43013
|
init_secret();
|
|
42765
|
-
|
|
43014
|
+
init_format();
|
|
43015
|
+
init_cancel();
|
|
42766
43016
|
}
|
|
42767
43017
|
});
|
|
42768
43018
|
|
|
@@ -43443,11 +43693,11 @@ var reducers = {
|
|
|
43443
43693
|
return { ...state, options: [{ name: `-c`, value: String(command) }] };
|
|
43444
43694
|
}
|
|
43445
43695
|
},
|
|
43446
|
-
setError: (state, segment, segmentIndex,
|
|
43696
|
+
setError: (state, segment, segmentIndex, errorMessage2) => {
|
|
43447
43697
|
if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
|
|
43448
|
-
return { ...state, errorMessage: `${
|
|
43698
|
+
return { ...state, errorMessage: `${errorMessage2}.` };
|
|
43449
43699
|
} else {
|
|
43450
|
-
return { ...state, errorMessage: `${
|
|
43700
|
+
return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
|
|
43451
43701
|
}
|
|
43452
43702
|
},
|
|
43453
43703
|
setOptionArityError: (state, segment) => {
|
|
@@ -44480,44 +44730,48 @@ var AuditListCommand = class extends Command {
|
|
|
44480
44730
|
sinceId = options_exports.String("--since-id");
|
|
44481
44731
|
json = options_exports.Boolean("--json", false);
|
|
44482
44732
|
async execute() {
|
|
44483
|
-
|
|
44484
|
-
|
|
44485
|
-
|
|
44486
|
-
|
|
44487
|
-
|
|
44488
|
-
|
|
44489
|
-
|
|
44490
|
-
|
|
44733
|
+
try {
|
|
44734
|
+
const client = new ApiClient();
|
|
44735
|
+
const data = await client.request(
|
|
44736
|
+
"/v1/audit",
|
|
44737
|
+
{
|
|
44738
|
+
query: {
|
|
44739
|
+
event_type: this.eventType,
|
|
44740
|
+
limit: this.limit,
|
|
44741
|
+
since_id: this.sinceId
|
|
44742
|
+
}
|
|
44491
44743
|
}
|
|
44492
|
-
|
|
44493
|
-
|
|
44494
|
-
|
|
44495
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
44744
|
+
);
|
|
44745
|
+
if (this.json) {
|
|
44746
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
44496
44747
|
`);
|
|
44497
|
-
|
|
44498
|
-
|
|
44499
|
-
|
|
44500
|
-
|
|
44501
|
-
|
|
44502
|
-
|
|
44503
|
-
|
|
44504
|
-
|
|
44505
|
-
|
|
44506
|
-
|
|
44507
|
-
|
|
44508
|
-
|
|
44509
|
-
|
|
44510
|
-
|
|
44511
|
-
|
|
44512
|
-
|
|
44513
|
-
|
|
44748
|
+
return 0;
|
|
44749
|
+
}
|
|
44750
|
+
if (data.entries.length === 0) {
|
|
44751
|
+
this.context.stdout.write("no audit entries\n");
|
|
44752
|
+
return 0;
|
|
44753
|
+
}
|
|
44754
|
+
this.context.stdout.write(
|
|
44755
|
+
`${table(
|
|
44756
|
+
["id", "ts", "actor", "agent", "event"],
|
|
44757
|
+
data.entries.map((e2) => [
|
|
44758
|
+
String(e2.id),
|
|
44759
|
+
e2.ts,
|
|
44760
|
+
e2.actor_user_id ?? "(none)",
|
|
44761
|
+
e2.actor_agent,
|
|
44762
|
+
e2.event_type
|
|
44763
|
+
])
|
|
44764
|
+
)}
|
|
44514
44765
|
`
|
|
44515
|
-
|
|
44516
|
-
|
|
44517
|
-
|
|
44766
|
+
);
|
|
44767
|
+
if (data.next_cursor) {
|
|
44768
|
+
this.context.stdout.write(`(next: --since-id ${data.next_cursor})
|
|
44518
44769
|
`);
|
|
44770
|
+
}
|
|
44771
|
+
return 0;
|
|
44772
|
+
} catch (err) {
|
|
44773
|
+
return handleExecError(this.context.stderr, err);
|
|
44519
44774
|
}
|
|
44520
|
-
return 0;
|
|
44521
44775
|
}
|
|
44522
44776
|
};
|
|
44523
44777
|
var AuditVerifyCommand = class extends Command {
|
|
@@ -44527,23 +44781,27 @@ var AuditVerifyCommand = class extends Command {
|
|
|
44527
44781
|
});
|
|
44528
44782
|
json = options_exports.Boolean("--json", false);
|
|
44529
44783
|
async execute() {
|
|
44530
|
-
|
|
44531
|
-
|
|
44532
|
-
|
|
44533
|
-
this.
|
|
44784
|
+
try {
|
|
44785
|
+
const client = new ApiClient();
|
|
44786
|
+
const data = await client.request("/v1/audit/verify", { method: "POST" });
|
|
44787
|
+
if (this.json) {
|
|
44788
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
44534
44789
|
`);
|
|
44535
|
-
|
|
44536
|
-
|
|
44537
|
-
|
|
44538
|
-
|
|
44790
|
+
return data.ok ? 0 : 1;
|
|
44791
|
+
}
|
|
44792
|
+
if (data.ok) {
|
|
44793
|
+
this.context.stdout.write(`OK: ${data.checked} entries verified
|
|
44539
44794
|
`);
|
|
44540
|
-
|
|
44541
|
-
|
|
44542
|
-
|
|
44543
|
-
|
|
44795
|
+
return 0;
|
|
44796
|
+
}
|
|
44797
|
+
this.context.stdout.write(
|
|
44798
|
+
`FAIL: chain broken at id ${data.broken_at_id} (${data.reason}); ${data.checked} entries verified before break
|
|
44544
44799
|
`
|
|
44545
|
-
|
|
44546
|
-
|
|
44800
|
+
);
|
|
44801
|
+
return 1;
|
|
44802
|
+
} catch (err) {
|
|
44803
|
+
return handleExecError(this.context.stderr, err);
|
|
44804
|
+
}
|
|
44547
44805
|
}
|
|
44548
44806
|
};
|
|
44549
44807
|
|
|
@@ -44584,11 +44842,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
|
|
|
44584
44842
|
return resolved;
|
|
44585
44843
|
}
|
|
44586
44844
|
function substitute(text, resolved) {
|
|
44845
|
+
const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
|
|
44587
44846
|
let out = text;
|
|
44588
|
-
for (const r of
|
|
44589
|
-
|
|
44590
|
-
out = out.split(r.alias.literal).join(r.value);
|
|
44591
|
-
}
|
|
44847
|
+
for (const r of sorted) {
|
|
44848
|
+
out = out.split(r.alias.literal).join(r.value);
|
|
44592
44849
|
}
|
|
44593
44850
|
return out;
|
|
44594
44851
|
}
|
|
@@ -44691,10 +44948,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
|
|
|
44691
44948
|
|
|
44692
44949
|
// ../../packages/redactor/dist/entropy.js
|
|
44693
44950
|
var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
|
|
44951
|
+
var DEFAULT_EXCLUDE_PREFIXES = [
|
|
44952
|
+
"sha1-",
|
|
44953
|
+
"sha256:",
|
|
44954
|
+
"sha256-",
|
|
44955
|
+
"sha384-",
|
|
44956
|
+
"sha512-"
|
|
44957
|
+
];
|
|
44694
44958
|
var DEFAULTS = {
|
|
44695
44959
|
enabled: true,
|
|
44696
44960
|
minLength: 24,
|
|
44697
|
-
minBitsPerChar: 4.5
|
|
44961
|
+
minBitsPerChar: 4.5,
|
|
44962
|
+
excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
|
|
44698
44963
|
};
|
|
44699
44964
|
function shannonEntropy(s) {
|
|
44700
44965
|
if (s.length === 0)
|
|
@@ -44727,9 +44992,13 @@ function findEntropyMatches(text, opts = {}) {
|
|
|
44727
44992
|
const end = i;
|
|
44728
44993
|
const token = text.slice(start, end);
|
|
44729
44994
|
if (token.length >= cfg.minLength) {
|
|
44730
|
-
const
|
|
44731
|
-
|
|
44732
|
-
|
|
44995
|
+
const lower = token.toLowerCase();
|
|
44996
|
+
const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
|
|
44997
|
+
if (!excluded) {
|
|
44998
|
+
const h2 = shannonEntropy(token);
|
|
44999
|
+
if (h2 >= cfg.minBitsPerChar) {
|
|
45000
|
+
matches.push({ start, end, token });
|
|
45001
|
+
}
|
|
44733
45002
|
}
|
|
44734
45003
|
}
|
|
44735
45004
|
}
|
|
@@ -44873,7 +45142,19 @@ var ENV_ALLOWLIST = [
|
|
|
44873
45142
|
"PWD",
|
|
44874
45143
|
"OLDPWD",
|
|
44875
45144
|
"TMPDIR",
|
|
44876
|
-
"SSH_AUTH_SOCK"
|
|
45145
|
+
"SSH_AUTH_SOCK",
|
|
45146
|
+
// Windows-specific
|
|
45147
|
+
"USERPROFILE",
|
|
45148
|
+
"USERNAME",
|
|
45149
|
+
"COMPUTERNAME",
|
|
45150
|
+
"TEMP",
|
|
45151
|
+
"TMP",
|
|
45152
|
+
"SYSTEMROOT",
|
|
45153
|
+
"SYSTEMDRIVE",
|
|
45154
|
+
"WINDIR",
|
|
45155
|
+
"COMSPEC",
|
|
45156
|
+
"APPDATA",
|
|
45157
|
+
"LOCALAPPDATA"
|
|
44877
45158
|
];
|
|
44878
45159
|
function spawnPrivileged(opts) {
|
|
44879
45160
|
const startedAt = Date.now();
|
|
@@ -44886,7 +45167,32 @@ function spawnPrivileged(opts) {
|
|
|
44886
45167
|
for (const [k2, v2] of Object.entries(opts.injectedEnv)) env2[k2] = v2;
|
|
44887
45168
|
}
|
|
44888
45169
|
const stdio = ["inherit", "pipe", "pipe"];
|
|
44889
|
-
const
|
|
45170
|
+
const WIN_BUILTINS = /* @__PURE__ */ new Set([
|
|
45171
|
+
"echo",
|
|
45172
|
+
"dir",
|
|
45173
|
+
"type",
|
|
45174
|
+
"copy",
|
|
45175
|
+
"del",
|
|
45176
|
+
"move",
|
|
45177
|
+
"ren",
|
|
45178
|
+
"md",
|
|
45179
|
+
"mkdir",
|
|
45180
|
+
"rd",
|
|
45181
|
+
"rmdir",
|
|
45182
|
+
"cd",
|
|
45183
|
+
"cls",
|
|
45184
|
+
"set",
|
|
45185
|
+
"pause",
|
|
45186
|
+
"find",
|
|
45187
|
+
"where"
|
|
45188
|
+
]);
|
|
45189
|
+
let spawnCmd = opts.command;
|
|
45190
|
+
let spawnArgs = opts.args;
|
|
45191
|
+
if (process.platform === "win32" && WIN_BUILTINS.has(opts.command.toLowerCase())) {
|
|
45192
|
+
spawnCmd = process.env.COMSPEC ?? "cmd.exe";
|
|
45193
|
+
spawnArgs = ["/d", "/s", "/c", opts.command, ...opts.args];
|
|
45194
|
+
}
|
|
45195
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
44890
45196
|
env: env2,
|
|
44891
45197
|
stdio,
|
|
44892
45198
|
detached: false
|
|
@@ -44953,10 +45259,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
|
|
|
44953
45259
|
spelled \`--from\` to avoid the collision.)
|
|
44954
45260
|
`,
|
|
44955
45261
|
examples: [
|
|
44956
|
-
[
|
|
44957
|
-
"Auto-load .keynv.env from cwd or parents",
|
|
44958
|
-
"$0 exec -- next dev"
|
|
44959
|
-
],
|
|
45262
|
+
["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
|
|
44960
45263
|
[
|
|
44961
45264
|
"Run mysql with the alias substituted at fork-exec time",
|
|
44962
45265
|
"$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
|
|
@@ -45020,7 +45323,18 @@ spelled \`--from\` to avoid the collision.)
|
|
|
45020
45323
|
`);
|
|
45021
45324
|
return 2;
|
|
45022
45325
|
}
|
|
45023
|
-
|
|
45326
|
+
this.context.stderr.write(
|
|
45327
|
+
`keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
|
|
45328
|
+
`
|
|
45329
|
+
);
|
|
45330
|
+
return 2;
|
|
45331
|
+
}
|
|
45332
|
+
if (!envFileLoaded && !this.noEnvFile && !this.envFile && !process.env.KEYNV_ENV_FILE) {
|
|
45333
|
+
this.context.stderr.write(
|
|
45334
|
+
`keynv: no ${ENV_FILE_BASENAME} found in this directory or any parent.
|
|
45335
|
+
Run \`keynv init\` in your project root to migrate secrets and create one.
|
|
45336
|
+
`
|
|
45337
|
+
);
|
|
45024
45338
|
}
|
|
45025
45339
|
const viaEnvSpecs = [];
|
|
45026
45340
|
for (const spec of this.viaEnv ?? []) {
|
|
@@ -45092,11 +45406,18 @@ spelled \`--from\` to avoid the collision.)
|
|
|
45092
45406
|
injectedEnv[spec.name] = value;
|
|
45093
45407
|
}
|
|
45094
45408
|
if (envFileLoaded && !this.quiet) {
|
|
45095
|
-
const
|
|
45096
|
-
const
|
|
45409
|
+
const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
|
|
45410
|
+
const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
|
|
45411
|
+
const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
|
|
45412
|
+
const parts = [];
|
|
45413
|
+
if (aliasEntries.length > 0) {
|
|
45414
|
+
parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
|
|
45415
|
+
}
|
|
45416
|
+
if (plainEntries.length > 0) {
|
|
45417
|
+
parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
|
|
45418
|
+
}
|
|
45097
45419
|
this.context.stderr.write(
|
|
45098
|
-
`keynv: loaded ${
|
|
45099
|
-
`
|
|
45420
|
+
`keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
|
|
45100
45421
|
);
|
|
45101
45422
|
}
|
|
45102
45423
|
const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
|
|
@@ -45136,10 +45457,158 @@ function signalNumber(sig) {
|
|
|
45136
45457
|
}
|
|
45137
45458
|
|
|
45138
45459
|
// src/commands/init.ts
|
|
45460
|
+
init_dist();
|
|
45139
45461
|
init_http();
|
|
45140
|
-
|
|
45462
|
+
init_envFile();
|
|
45463
|
+
init_heuristics();
|
|
45141
45464
|
init_init();
|
|
45142
45465
|
init_cancel();
|
|
45466
|
+
init_tty();
|
|
45467
|
+
|
|
45468
|
+
// src/commands/project.ts
|
|
45469
|
+
init_http();
|
|
45470
|
+
init_format();
|
|
45471
|
+
function isProjectId(input) {
|
|
45472
|
+
return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
|
|
45473
|
+
}
|
|
45474
|
+
async function resolveProjectId(client, input) {
|
|
45475
|
+
if (isProjectId(input)) {
|
|
45476
|
+
return input;
|
|
45477
|
+
}
|
|
45478
|
+
const data = await client.request("/v1/projects");
|
|
45479
|
+
const match = data.projects.find((p2) => p2.name === input);
|
|
45480
|
+
if (!match) throw new Error(`project not found: ${input}`);
|
|
45481
|
+
return match.id;
|
|
45482
|
+
}
|
|
45483
|
+
var ProjectListCommand = class extends Command {
|
|
45484
|
+
static paths = [["project", "list"]];
|
|
45485
|
+
static usage = Command.Usage({
|
|
45486
|
+
description: "List projects visible to the current user."
|
|
45487
|
+
});
|
|
45488
|
+
json = options_exports.Boolean("--json", false);
|
|
45489
|
+
async execute() {
|
|
45490
|
+
try {
|
|
45491
|
+
const client = new ApiClient();
|
|
45492
|
+
const data = await client.request("/v1/projects");
|
|
45493
|
+
if (this.json) {
|
|
45494
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45495
|
+
`);
|
|
45496
|
+
return 0;
|
|
45497
|
+
}
|
|
45498
|
+
if (data.projects.length === 0) {
|
|
45499
|
+
this.context.stdout.write("no projects\n");
|
|
45500
|
+
return 0;
|
|
45501
|
+
}
|
|
45502
|
+
this.context.stdout.write(
|
|
45503
|
+
`${table(
|
|
45504
|
+
["name", "id", "created_at"],
|
|
45505
|
+
data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
|
|
45506
|
+
)}
|
|
45507
|
+
`
|
|
45508
|
+
);
|
|
45509
|
+
return 0;
|
|
45510
|
+
} catch (err) {
|
|
45511
|
+
return handleExecError(this.context.stderr, err);
|
|
45512
|
+
}
|
|
45513
|
+
}
|
|
45514
|
+
};
|
|
45515
|
+
var ProjectCreateCommand = class extends Command {
|
|
45516
|
+
static paths = [["project", "create"]];
|
|
45517
|
+
static usage = Command.Usage({
|
|
45518
|
+
description: "Create a new project with one or more environments.",
|
|
45519
|
+
examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
|
|
45520
|
+
});
|
|
45521
|
+
name = options_exports.String();
|
|
45522
|
+
envs = options_exports.Array("--env", {
|
|
45523
|
+
description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
|
|
45524
|
+
});
|
|
45525
|
+
async execute() {
|
|
45526
|
+
try {
|
|
45527
|
+
const envs = (this.envs ?? ["dev"]).map((spec) => {
|
|
45528
|
+
const [name, tier, approval] = spec.split(":");
|
|
45529
|
+
return {
|
|
45530
|
+
name: name ?? "",
|
|
45531
|
+
tier: tier ?? "non-production",
|
|
45532
|
+
require_approval: approval === "approval"
|
|
45533
|
+
};
|
|
45534
|
+
});
|
|
45535
|
+
const client = new ApiClient();
|
|
45536
|
+
const result = await client.request("/v1/projects", {
|
|
45537
|
+
method: "POST",
|
|
45538
|
+
body: { name: this.name, environments: envs }
|
|
45539
|
+
});
|
|
45540
|
+
this.context.stdout.write(`created project ${result.name} (${result.id})
|
|
45541
|
+
`);
|
|
45542
|
+
for (const e2 of envs) {
|
|
45543
|
+
this.context.stdout.write(
|
|
45544
|
+
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45545
|
+
`
|
|
45546
|
+
);
|
|
45547
|
+
}
|
|
45548
|
+
return 0;
|
|
45549
|
+
} catch (err) {
|
|
45550
|
+
return handleExecError(this.context.stderr, err);
|
|
45551
|
+
}
|
|
45552
|
+
}
|
|
45553
|
+
};
|
|
45554
|
+
var ProjectDescribeCommand = class extends Command {
|
|
45555
|
+
static paths = [["project", "describe"]];
|
|
45556
|
+
static usage = Command.Usage({
|
|
45557
|
+
description: "Show metadata for one project.",
|
|
45558
|
+
examples: [
|
|
45559
|
+
["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
|
|
45560
|
+
["By name", "$0 project describe myproject"]
|
|
45561
|
+
]
|
|
45562
|
+
});
|
|
45563
|
+
id = options_exports.String();
|
|
45564
|
+
json = options_exports.Boolean("--json", false);
|
|
45565
|
+
async execute() {
|
|
45566
|
+
try {
|
|
45567
|
+
const client = new ApiClient();
|
|
45568
|
+
const projectId2 = await resolveProjectId(client, this.id);
|
|
45569
|
+
const data = await client.request(`/v1/projects/${projectId2}`);
|
|
45570
|
+
if (this.json) {
|
|
45571
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45572
|
+
`);
|
|
45573
|
+
return 0;
|
|
45574
|
+
}
|
|
45575
|
+
this.context.stdout.write(`project: ${data.name} (${data.id})
|
|
45576
|
+
`);
|
|
45577
|
+
for (const e2 of data.environments) {
|
|
45578
|
+
this.context.stdout.write(
|
|
45579
|
+
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45580
|
+
`
|
|
45581
|
+
);
|
|
45582
|
+
}
|
|
45583
|
+
return 0;
|
|
45584
|
+
} catch (err) {
|
|
45585
|
+
return handleExecError(this.context.stderr, err);
|
|
45586
|
+
}
|
|
45587
|
+
}
|
|
45588
|
+
};
|
|
45589
|
+
var ProjectDeleteCommand = class extends Command {
|
|
45590
|
+
static paths = [["project", "delete"]];
|
|
45591
|
+
static usage = Command.Usage({ description: "Soft-delete a project." });
|
|
45592
|
+
id = options_exports.String();
|
|
45593
|
+
force = options_exports.Boolean("--force", false);
|
|
45594
|
+
async execute() {
|
|
45595
|
+
if (!this.force) {
|
|
45596
|
+
this.context.stderr.write("keynv: refusing to delete without --force\n");
|
|
45597
|
+
return 2;
|
|
45598
|
+
}
|
|
45599
|
+
try {
|
|
45600
|
+
const client = new ApiClient();
|
|
45601
|
+
await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
|
|
45602
|
+
this.context.stdout.write(`deleted project ${this.id}
|
|
45603
|
+
`);
|
|
45604
|
+
return 0;
|
|
45605
|
+
} catch (err) {
|
|
45606
|
+
return handleExecError(this.context.stderr, err);
|
|
45607
|
+
}
|
|
45608
|
+
}
|
|
45609
|
+
};
|
|
45610
|
+
|
|
45611
|
+
// src/commands/init.ts
|
|
45143
45612
|
var InitCommand = class extends Command {
|
|
45144
45613
|
static paths = [["init"]];
|
|
45145
45614
|
static usage = Command.Usage({
|
|
@@ -45153,6 +45622,11 @@ package.json scripts with \`keynv exec\`.
|
|
|
45153
45622
|
Safe to re-run: existing .keynv.env entries are preserved; new
|
|
45154
45623
|
entries are appended below a marker.
|
|
45155
45624
|
|
|
45625
|
+
--dry-run prints the secrets that would be uploaded and the alias
|
|
45626
|
+
mappings that would be written to .keynv.env, then exits without
|
|
45627
|
+
touching any files or making any network calls. Use it to preview
|
|
45628
|
+
what init will do before committing.
|
|
45629
|
+
|
|
45156
45630
|
Requires an interactive terminal (clack TUI). For scripted
|
|
45157
45631
|
migration, use the lower-level \`keynv project\` and \`keynv secret\`
|
|
45158
45632
|
commands directly.
|
|
@@ -45160,28 +45634,47 @@ commands directly.
|
|
|
45160
45634
|
examples: [
|
|
45161
45635
|
["Walk the current project", "$0 init"],
|
|
45162
45636
|
["Preview without writing or uploading", "$0 init --dry-run"],
|
|
45163
|
-
["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
|
|
45637
|
+
["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
|
|
45638
|
+
["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
|
|
45164
45639
|
]
|
|
45165
45640
|
});
|
|
45166
45641
|
dryRun = options_exports.Boolean("--dry-run", false, {
|
|
45167
|
-
description: "
|
|
45642
|
+
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."
|
|
45168
45643
|
});
|
|
45169
45644
|
noScripts = options_exports.Boolean("--no-scripts", false, {
|
|
45170
45645
|
description: "Skip the package.json script-wrapping step."
|
|
45171
45646
|
});
|
|
45647
|
+
envFile = options_exports.String("--env-file", {
|
|
45648
|
+
description: "Path to .env file to migrate (non-interactive mode)."
|
|
45649
|
+
});
|
|
45650
|
+
project = options_exports.String("--project", {
|
|
45651
|
+
description: "Project name or ID (non-interactive mode)."
|
|
45652
|
+
});
|
|
45653
|
+
env = options_exports.String("--env", {
|
|
45654
|
+
description: "Environment name for --env-file (default: dev)."
|
|
45655
|
+
});
|
|
45656
|
+
secret = options_exports.Array("--secret", {
|
|
45657
|
+
description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
|
|
45658
|
+
});
|
|
45172
45659
|
async execute() {
|
|
45173
|
-
if (!isInteractive()) {
|
|
45174
|
-
this.context.stderr.write(
|
|
45175
|
-
"keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
|
|
45176
|
-
);
|
|
45177
|
-
return 1;
|
|
45178
|
-
}
|
|
45179
45660
|
const client = new ApiClient();
|
|
45180
45661
|
await client.ensureHydrated();
|
|
45181
45662
|
if (!client.isLoggedIn) {
|
|
45182
45663
|
this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
|
|
45183
45664
|
return 1;
|
|
45184
45665
|
}
|
|
45666
|
+
const hasEnvFile = this.envFile != null;
|
|
45667
|
+
const hasSecrets = this.secret != null && this.secret.length > 0;
|
|
45668
|
+
const isNonInteractive = hasEnvFile || hasSecrets;
|
|
45669
|
+
if (isNonInteractive) {
|
|
45670
|
+
return this.runNonInteractive(client);
|
|
45671
|
+
}
|
|
45672
|
+
if (!isInteractive()) {
|
|
45673
|
+
this.context.stderr.write(
|
|
45674
|
+
"keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
|
|
45675
|
+
);
|
|
45676
|
+
return 1;
|
|
45677
|
+
}
|
|
45185
45678
|
try {
|
|
45186
45679
|
const outcome = await runInitFlow(client, {
|
|
45187
45680
|
cwd: process.cwd(),
|
|
@@ -45197,9 +45690,102 @@ commands directly.
|
|
|
45197
45690
|
return 1;
|
|
45198
45691
|
}
|
|
45199
45692
|
}
|
|
45693
|
+
async runNonInteractive(client) {
|
|
45694
|
+
const projectName = this.project;
|
|
45695
|
+
if (!projectName) {
|
|
45696
|
+
this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
|
|
45697
|
+
return 1;
|
|
45698
|
+
}
|
|
45699
|
+
const resolved = await resolveProjectId(client, projectName);
|
|
45700
|
+
if (!resolved) {
|
|
45701
|
+
this.context.stderr.write(`keynv: project not found: ${projectName}
|
|
45702
|
+
`);
|
|
45703
|
+
return 1;
|
|
45704
|
+
}
|
|
45705
|
+
const projectId2 = resolved;
|
|
45706
|
+
const envName = this.env ?? "dev";
|
|
45707
|
+
const secrets = [];
|
|
45708
|
+
if (this.envFile) {
|
|
45709
|
+
try {
|
|
45710
|
+
const content = readFileSync(this.envFile, "utf8");
|
|
45711
|
+
const entries = parseEnvFile(content, this.envFile);
|
|
45712
|
+
for (const e2 of entries) {
|
|
45713
|
+
if (classifyEntry(e2.name, e2.value).verdict === "secret") {
|
|
45714
|
+
secrets.push({ name: e2.name, value: e2.value });
|
|
45715
|
+
}
|
|
45716
|
+
}
|
|
45717
|
+
} catch (err) {
|
|
45718
|
+
this.context.stderr.write(
|
|
45719
|
+
`keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
|
|
45720
|
+
`
|
|
45721
|
+
);
|
|
45722
|
+
return 1;
|
|
45723
|
+
}
|
|
45724
|
+
}
|
|
45725
|
+
if (this.secret) {
|
|
45726
|
+
for (const spec of this.secret) {
|
|
45727
|
+
const eq = spec.indexOf("=");
|
|
45728
|
+
if (eq <= 0) {
|
|
45729
|
+
this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
|
|
45730
|
+
`);
|
|
45731
|
+
return 1;
|
|
45732
|
+
}
|
|
45733
|
+
const name = spec.slice(0, eq);
|
|
45734
|
+
const value = spec.slice(eq + 1);
|
|
45735
|
+
secrets.push({ name, value });
|
|
45736
|
+
}
|
|
45737
|
+
}
|
|
45738
|
+
if (secrets.length === 0) {
|
|
45739
|
+
this.context.stdout.write("keynv: nothing to migrate.\n");
|
|
45740
|
+
return 0;
|
|
45741
|
+
}
|
|
45742
|
+
if (this.dryRun) {
|
|
45743
|
+
this.context.stdout.write(
|
|
45744
|
+
`keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
|
|
45745
|
+
`
|
|
45746
|
+
);
|
|
45747
|
+
for (const { name } of secrets) {
|
|
45748
|
+
const aliasKey = name.toLowerCase().replace(/_/g, "-");
|
|
45749
|
+
this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
|
|
45750
|
+
`);
|
|
45751
|
+
}
|
|
45752
|
+
return 0;
|
|
45753
|
+
}
|
|
45754
|
+
let uploaded = 0;
|
|
45755
|
+
const failed = [];
|
|
45756
|
+
for (const { name, value } of secrets) {
|
|
45757
|
+
const aliasKey = name.toLowerCase().replace(/_/g, "-");
|
|
45758
|
+
const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
|
|
45759
|
+
if (!alias2) {
|
|
45760
|
+
failed.push(`${name} (invalid alias key: ${aliasKey})`);
|
|
45761
|
+
continue;
|
|
45762
|
+
}
|
|
45763
|
+
try {
|
|
45764
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
45765
|
+
method: "POST",
|
|
45766
|
+
body: { env: envName, key: aliasKey, value }
|
|
45767
|
+
});
|
|
45768
|
+
uploaded++;
|
|
45769
|
+
} catch (err) {
|
|
45770
|
+
failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45771
|
+
}
|
|
45772
|
+
}
|
|
45773
|
+
this.context.stdout.write(
|
|
45774
|
+
`keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
|
|
45775
|
+
`
|
|
45776
|
+
);
|
|
45777
|
+
if (failed.length > 0) {
|
|
45778
|
+
for (const f of failed) this.context.stderr.write(` failed: ${f}
|
|
45779
|
+
`);
|
|
45780
|
+
return 1;
|
|
45781
|
+
}
|
|
45782
|
+
return 0;
|
|
45783
|
+
}
|
|
45200
45784
|
};
|
|
45201
45785
|
|
|
45202
45786
|
// src/commands/login.ts
|
|
45787
|
+
init_browser_auth();
|
|
45788
|
+
init_defaults();
|
|
45203
45789
|
init_http();
|
|
45204
45790
|
init_store();
|
|
45205
45791
|
init_format();
|
|
@@ -45260,25 +45846,63 @@ var LoginCommand = class extends Command {
|
|
|
45260
45846
|
static usage = Command.Usage({
|
|
45261
45847
|
description: "Authenticate against a keynv server.",
|
|
45262
45848
|
examples: [
|
|
45263
|
-
["
|
|
45849
|
+
["Browser login", "$0 login"],
|
|
45850
|
+
["Self-hosted browser login", "$0 login --server http://localhost:8080"],
|
|
45851
|
+
["Headless token login", "$0 login --server http://... --token kt_..."],
|
|
45264
45852
|
["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
|
|
45265
45853
|
]
|
|
45266
45854
|
});
|
|
45267
45855
|
server = options_exports.String("--server", { description: "Server base URL." });
|
|
45856
|
+
token = options_exports.String("--token", { description: "CLI token for headless auth." });
|
|
45268
45857
|
email = options_exports.String("--email", { description: "Email address." });
|
|
45269
45858
|
password = options_exports.String("--password", {
|
|
45270
45859
|
description: "Password (use stdin to avoid argv leak)."
|
|
45271
45860
|
});
|
|
45272
45861
|
async execute() {
|
|
45273
|
-
const
|
|
45274
|
-
|
|
45862
|
+
const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
|
|
45863
|
+
try {
|
|
45864
|
+
new URL(finalServerUrl);
|
|
45865
|
+
} catch {
|
|
45866
|
+
this.context.stderr.write(`keynv: invalid server URL '${finalServerUrl}'
|
|
45867
|
+
`);
|
|
45868
|
+
return 1;
|
|
45869
|
+
}
|
|
45870
|
+
if (this.token) {
|
|
45871
|
+
return this.loginWithToken(finalServerUrl, this.token);
|
|
45872
|
+
}
|
|
45873
|
+
if (!this.email && !this.password) {
|
|
45874
|
+
this.context.stdout.write(`Opening browser for ${finalServerUrl} ...
|
|
45875
|
+
`);
|
|
45876
|
+
try {
|
|
45877
|
+
const creds = await runBrowserAuth(finalServerUrl);
|
|
45878
|
+
await saveCredentials(creds);
|
|
45879
|
+
this.context.stdout.write(`logged in as ${creds.email} (${creds.org_role})
|
|
45880
|
+
`);
|
|
45881
|
+
return 0;
|
|
45882
|
+
} catch (err) {
|
|
45883
|
+
this.context.stderr.write(
|
|
45884
|
+
`keynv: ${err instanceof Error ? err.message : "browser login failed"}
|
|
45885
|
+
`
|
|
45886
|
+
);
|
|
45887
|
+
return 1;
|
|
45888
|
+
}
|
|
45889
|
+
}
|
|
45275
45890
|
const email2 = this.email ?? await promptLine("email: ");
|
|
45276
45891
|
const password = this.password ?? await promptHidden("password: ");
|
|
45277
|
-
|
|
45278
|
-
|
|
45279
|
-
|
|
45280
|
-
|
|
45281
|
-
|
|
45892
|
+
let res;
|
|
45893
|
+
try {
|
|
45894
|
+
res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
|
|
45895
|
+
method: "POST",
|
|
45896
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
45897
|
+
body: JSON.stringify({ email: email2, password })
|
|
45898
|
+
});
|
|
45899
|
+
} catch (err) {
|
|
45900
|
+
this.context.stderr.write(
|
|
45901
|
+
`keynv: cannot reach server '${finalServerUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
45902
|
+
`
|
|
45903
|
+
);
|
|
45904
|
+
return 1;
|
|
45905
|
+
}
|
|
45282
45906
|
if (!res.ok) {
|
|
45283
45907
|
const text = await res.text();
|
|
45284
45908
|
let msg = `login failed (${res.status})`;
|
|
@@ -45302,6 +45926,7 @@ var LoginCommand = class extends Command {
|
|
|
45302
45926
|
const data = await res.json();
|
|
45303
45927
|
try {
|
|
45304
45928
|
await saveCredentials({
|
|
45929
|
+
auth_kind: "session",
|
|
45305
45930
|
server_url: finalServerUrl,
|
|
45306
45931
|
user_id: data.user.id,
|
|
45307
45932
|
email: data.user.email,
|
|
@@ -45319,6 +45944,63 @@ var LoginCommand = class extends Command {
|
|
|
45319
45944
|
return 1;
|
|
45320
45945
|
}
|
|
45321
45946
|
this.context.stdout.write(`logged in as ${data.user.email} (${data.user.org_role})
|
|
45947
|
+
`);
|
|
45948
|
+
return 0;
|
|
45949
|
+
}
|
|
45950
|
+
async loginWithToken(serverUrl, token) {
|
|
45951
|
+
let res;
|
|
45952
|
+
try {
|
|
45953
|
+
res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
|
|
45954
|
+
headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
|
|
45955
|
+
});
|
|
45956
|
+
} catch (err) {
|
|
45957
|
+
this.context.stderr.write(
|
|
45958
|
+
`keynv: cannot reach server '${serverUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
45959
|
+
`
|
|
45960
|
+
);
|
|
45961
|
+
return 1;
|
|
45962
|
+
}
|
|
45963
|
+
if (!res.ok) {
|
|
45964
|
+
const text = await res.text();
|
|
45965
|
+
let msg = `token login failed (${res.status})`;
|
|
45966
|
+
try {
|
|
45967
|
+
const parsed = JSON.parse(text);
|
|
45968
|
+
msg = parsed.error?.message ?? msg;
|
|
45969
|
+
this.context.stderr.write(
|
|
45970
|
+
`${fmtError({
|
|
45971
|
+
status: res.status,
|
|
45972
|
+
...parsed.error?.code ? { code: parsed.error.code } : {},
|
|
45973
|
+
message: msg
|
|
45974
|
+
})}
|
|
45975
|
+
`
|
|
45976
|
+
);
|
|
45977
|
+
} catch {
|
|
45978
|
+
this.context.stderr.write(`keynv: ${msg}
|
|
45979
|
+
`);
|
|
45980
|
+
}
|
|
45981
|
+
return 1;
|
|
45982
|
+
}
|
|
45983
|
+
const data = await res.json();
|
|
45984
|
+
try {
|
|
45985
|
+
await saveCredentials({
|
|
45986
|
+
auth_kind: "cli_token",
|
|
45987
|
+
server_url: serverUrl,
|
|
45988
|
+
user_id: data.id,
|
|
45989
|
+
email: data.email,
|
|
45990
|
+
org_id: data.org_id,
|
|
45991
|
+
org_role: data.org_role,
|
|
45992
|
+
access_token: token,
|
|
45993
|
+
refresh_token: "",
|
|
45994
|
+
access_expires_at: "9999-12-31T23:59:59.999Z"
|
|
45995
|
+
});
|
|
45996
|
+
} catch (err) {
|
|
45997
|
+
this.context.stderr.write(
|
|
45998
|
+
`keynv: failed to persist credentials: ${err instanceof Error ? err.message : String(err)}
|
|
45999
|
+
`
|
|
46000
|
+
);
|
|
46001
|
+
return 1;
|
|
46002
|
+
}
|
|
46003
|
+
this.context.stdout.write(`logged in as ${data.email} (${data.org_role})
|
|
45322
46004
|
`);
|
|
45323
46005
|
return 0;
|
|
45324
46006
|
}
|
|
@@ -45355,48 +46037,46 @@ var WhoamiCommand = class extends Command {
|
|
|
45355
46037
|
});
|
|
45356
46038
|
json = options_exports.Boolean("--json", false);
|
|
45357
46039
|
async execute() {
|
|
45358
|
-
|
|
45359
|
-
|
|
45360
|
-
|
|
45361
|
-
|
|
45362
|
-
|
|
45363
|
-
|
|
45364
|
-
|
|
45365
|
-
|
|
45366
|
-
this.
|
|
46040
|
+
try {
|
|
46041
|
+
const client = new ApiClient();
|
|
46042
|
+
await client.ensureHydrated();
|
|
46043
|
+
if (!client.isLoggedIn) {
|
|
46044
|
+
this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
|
|
46045
|
+
return 1;
|
|
46046
|
+
}
|
|
46047
|
+
const data = await client.request("/v1/whoami");
|
|
46048
|
+
if (this.json) {
|
|
46049
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45367
46050
|
`);
|
|
45368
|
-
|
|
45369
|
-
|
|
45370
|
-
|
|
46051
|
+
return 0;
|
|
46052
|
+
}
|
|
46053
|
+
this.context.stdout.write(`user: ${data.email} (${data.id})
|
|
45371
46054
|
`);
|
|
45372
|
-
|
|
46055
|
+
const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
|
|
46056
|
+
this.context.stdout.write(`org: ${orgDisplay}
|
|
45373
46057
|
`);
|
|
45374
|
-
|
|
46058
|
+
this.context.stdout.write(`role: ${data.org_role}
|
|
45375
46059
|
`);
|
|
45376
|
-
|
|
45377
|
-
|
|
45378
|
-
|
|
45379
|
-
|
|
45380
|
-
|
|
45381
|
-
|
|
46060
|
+
if (data.memberships.length === 0) {
|
|
46061
|
+
this.context.stdout.write("memberships: (none)\n");
|
|
46062
|
+
} else {
|
|
46063
|
+
this.context.stdout.write("memberships:\n");
|
|
46064
|
+
for (const m of data.memberships) {
|
|
46065
|
+
const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
|
|
46066
|
+
this.context.stdout.write(` ${proj}: ${m.role}
|
|
45382
46067
|
`);
|
|
46068
|
+
}
|
|
45383
46069
|
}
|
|
46070
|
+
return 0;
|
|
46071
|
+
} catch (err) {
|
|
46072
|
+
return handleExecError(this.context.stderr, err);
|
|
45384
46073
|
}
|
|
45385
|
-
return 0;
|
|
45386
46074
|
}
|
|
45387
46075
|
};
|
|
45388
46076
|
|
|
45389
46077
|
// src/commands/member.ts
|
|
45390
46078
|
init_http();
|
|
45391
46079
|
init_format();
|
|
45392
|
-
async function findProjectIdByName(client, name) {
|
|
45393
|
-
const data = await client.request(
|
|
45394
|
-
"/v1/projects"
|
|
45395
|
-
);
|
|
45396
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
45397
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
45398
|
-
return match.id;
|
|
45399
|
-
}
|
|
45400
46080
|
var MemberAddCommand = class extends Command {
|
|
45401
46081
|
static paths = [["member", "add"]];
|
|
45402
46082
|
static usage = Command.Usage({
|
|
@@ -45412,15 +46092,19 @@ var MemberAddCommand = class extends Command {
|
|
|
45412
46092
|
this.context.stderr.write("keynv: --role must be one of lead, developer, reader\n");
|
|
45413
46093
|
return 1;
|
|
45414
46094
|
}
|
|
45415
|
-
|
|
45416
|
-
|
|
45417
|
-
|
|
45418
|
-
|
|
45419
|
-
|
|
45420
|
-
|
|
45421
|
-
|
|
46095
|
+
try {
|
|
46096
|
+
const client = new ApiClient();
|
|
46097
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
46098
|
+
await client.request(`/v1/projects/${projectId2}/members`, {
|
|
46099
|
+
method: "POST",
|
|
46100
|
+
body: { email: this.email, role: role2 }
|
|
46101
|
+
});
|
|
46102
|
+
this.context.stdout.write(`added ${this.email} to ${this.project} as ${role2}
|
|
45422
46103
|
`);
|
|
45423
|
-
|
|
46104
|
+
return 0;
|
|
46105
|
+
} catch (err) {
|
|
46106
|
+
return handleExecError(this.context.stderr, err);
|
|
46107
|
+
}
|
|
45424
46108
|
}
|
|
45425
46109
|
};
|
|
45426
46110
|
var MemberRemoveCommand = class extends Command {
|
|
@@ -45429,21 +46113,25 @@ var MemberRemoveCommand = class extends Command {
|
|
|
45429
46113
|
project = options_exports.String();
|
|
45430
46114
|
email = options_exports.String();
|
|
45431
46115
|
async execute() {
|
|
45432
|
-
|
|
45433
|
-
|
|
45434
|
-
|
|
45435
|
-
|
|
45436
|
-
|
|
45437
|
-
|
|
46116
|
+
try {
|
|
46117
|
+
const client = new ApiClient();
|
|
46118
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
46119
|
+
const members = await client.request(`/v1/projects/${projectId2}/members`);
|
|
46120
|
+
const target = members.members.find((m) => m.email === this.email);
|
|
46121
|
+
if (!target) {
|
|
46122
|
+
this.context.stderr.write(`keynv: ${this.email} is not a member of ${this.project}
|
|
45438
46123
|
`);
|
|
45439
|
-
|
|
45440
|
-
|
|
45441
|
-
|
|
45442
|
-
|
|
45443
|
-
|
|
45444
|
-
|
|
46124
|
+
return 1;
|
|
46125
|
+
}
|
|
46126
|
+
await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
|
|
46127
|
+
method: "DELETE"
|
|
46128
|
+
});
|
|
46129
|
+
this.context.stdout.write(`removed ${this.email} from ${this.project}
|
|
45445
46130
|
`);
|
|
45446
|
-
|
|
46131
|
+
return 0;
|
|
46132
|
+
} catch (err) {
|
|
46133
|
+
return handleExecError(this.context.stderr, err);
|
|
46134
|
+
}
|
|
45447
46135
|
}
|
|
45448
46136
|
};
|
|
45449
46137
|
var MemberListCommand = class extends Command {
|
|
@@ -45452,134 +46140,30 @@ var MemberListCommand = class extends Command {
|
|
|
45452
46140
|
project = options_exports.String();
|
|
45453
46141
|
json = options_exports.Boolean("--json", false);
|
|
45454
46142
|
async execute() {
|
|
45455
|
-
|
|
45456
|
-
|
|
45457
|
-
|
|
45458
|
-
|
|
45459
|
-
this.
|
|
45460
|
-
|
|
45461
|
-
return 0;
|
|
45462
|
-
}
|
|
45463
|
-
if (data.members.length === 0) {
|
|
45464
|
-
this.context.stdout.write("no members\n");
|
|
45465
|
-
return 0;
|
|
45466
|
-
}
|
|
45467
|
-
this.context.stdout.write(
|
|
45468
|
-
`${table(
|
|
45469
|
-
["email", "role", "granted_at"],
|
|
45470
|
-
data.members.map((m) => [m.email, m.role, m.granted_at])
|
|
45471
|
-
)}
|
|
45472
|
-
`
|
|
45473
|
-
);
|
|
45474
|
-
return 0;
|
|
45475
|
-
}
|
|
45476
|
-
};
|
|
45477
|
-
|
|
45478
|
-
// src/commands/project.ts
|
|
45479
|
-
init_http();
|
|
45480
|
-
init_format();
|
|
45481
|
-
var ProjectListCommand = class extends Command {
|
|
45482
|
-
static paths = [["project", "list"]];
|
|
45483
|
-
static usage = Command.Usage({
|
|
45484
|
-
description: "List projects visible to the current user."
|
|
45485
|
-
});
|
|
45486
|
-
json = options_exports.Boolean("--json", false);
|
|
45487
|
-
async execute() {
|
|
45488
|
-
const client = new ApiClient();
|
|
45489
|
-
const data = await client.request("/v1/projects");
|
|
45490
|
-
if (this.json) {
|
|
45491
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45492
|
-
`);
|
|
45493
|
-
return 0;
|
|
45494
|
-
}
|
|
45495
|
-
if (data.projects.length === 0) {
|
|
45496
|
-
this.context.stdout.write("no projects\n");
|
|
45497
|
-
return 0;
|
|
45498
|
-
}
|
|
45499
|
-
this.context.stdout.write(
|
|
45500
|
-
`${table(
|
|
45501
|
-
["name", "id", "created_at"],
|
|
45502
|
-
data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
|
|
45503
|
-
)}
|
|
45504
|
-
`
|
|
45505
|
-
);
|
|
45506
|
-
return 0;
|
|
45507
|
-
}
|
|
45508
|
-
};
|
|
45509
|
-
var ProjectCreateCommand = class extends Command {
|
|
45510
|
-
static paths = [["project", "create"]];
|
|
45511
|
-
static usage = Command.Usage({
|
|
45512
|
-
description: "Create a new project with one or more environments.",
|
|
45513
|
-
examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
|
|
45514
|
-
});
|
|
45515
|
-
name = options_exports.String();
|
|
45516
|
-
envs = options_exports.Array("--env", {
|
|
45517
|
-
description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
|
|
45518
|
-
});
|
|
45519
|
-
async execute() {
|
|
45520
|
-
const envs = (this.envs ?? ["dev"]).map((spec) => {
|
|
45521
|
-
const [name, tier, approval] = spec.split(":");
|
|
45522
|
-
return {
|
|
45523
|
-
name: name ?? "",
|
|
45524
|
-
tier: tier ?? "non-production",
|
|
45525
|
-
require_approval: approval === "approval"
|
|
45526
|
-
};
|
|
45527
|
-
});
|
|
45528
|
-
const client = new ApiClient();
|
|
45529
|
-
const result = await client.request("/v1/projects", {
|
|
45530
|
-
method: "POST",
|
|
45531
|
-
body: { name: this.name, environments: envs }
|
|
45532
|
-
});
|
|
45533
|
-
this.context.stdout.write(`created project ${result.name} (${result.id})
|
|
46143
|
+
try {
|
|
46144
|
+
const client = new ApiClient();
|
|
46145
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
46146
|
+
const data = await client.request(`/v1/projects/${projectId2}/members`);
|
|
46147
|
+
if (this.json) {
|
|
46148
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45534
46149
|
`);
|
|
45535
|
-
|
|
46150
|
+
return 0;
|
|
46151
|
+
}
|
|
46152
|
+
if (data.members.length === 0) {
|
|
46153
|
+
this.context.stdout.write("no members\n");
|
|
46154
|
+
return 0;
|
|
46155
|
+
}
|
|
45536
46156
|
this.context.stdout.write(
|
|
45537
|
-
|
|
46157
|
+
`${table(
|
|
46158
|
+
["email", "role", "granted_at"],
|
|
46159
|
+
data.members.map((m) => [m.email, m.role, m.granted_at])
|
|
46160
|
+
)}
|
|
45538
46161
|
`
|
|
45539
46162
|
);
|
|
45540
|
-
}
|
|
45541
|
-
return 0;
|
|
45542
|
-
}
|
|
45543
|
-
};
|
|
45544
|
-
var ProjectDescribeCommand = class extends Command {
|
|
45545
|
-
static paths = [["project", "describe"]];
|
|
45546
|
-
static usage = Command.Usage({ description: "Show metadata for one project." });
|
|
45547
|
-
id = options_exports.String();
|
|
45548
|
-
json = options_exports.Boolean("--json", false);
|
|
45549
|
-
async execute() {
|
|
45550
|
-
const client = new ApiClient();
|
|
45551
|
-
const data = await client.request(`/v1/projects/${this.id}`);
|
|
45552
|
-
if (this.json) {
|
|
45553
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45554
|
-
`);
|
|
45555
46163
|
return 0;
|
|
46164
|
+
} catch (err) {
|
|
46165
|
+
return handleExecError(this.context.stderr, err);
|
|
45556
46166
|
}
|
|
45557
|
-
this.context.stdout.write(`project: ${data.name} (${data.id})
|
|
45558
|
-
`);
|
|
45559
|
-
for (const e2 of data.environments) {
|
|
45560
|
-
this.context.stdout.write(
|
|
45561
|
-
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45562
|
-
`
|
|
45563
|
-
);
|
|
45564
|
-
}
|
|
45565
|
-
return 0;
|
|
45566
|
-
}
|
|
45567
|
-
};
|
|
45568
|
-
var ProjectDeleteCommand = class extends Command {
|
|
45569
|
-
static paths = [["project", "delete"]];
|
|
45570
|
-
static usage = Command.Usage({ description: "Soft-delete a project." });
|
|
45571
|
-
id = options_exports.String();
|
|
45572
|
-
force = options_exports.Boolean("--force", false);
|
|
45573
|
-
async execute() {
|
|
45574
|
-
if (!this.force) {
|
|
45575
|
-
this.context.stderr.write("keynv: refusing to delete without --force\n");
|
|
45576
|
-
return 2;
|
|
45577
|
-
}
|
|
45578
|
-
const client = new ApiClient();
|
|
45579
|
-
await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
|
|
45580
|
-
this.context.stdout.write(`deleted project ${this.id}
|
|
45581
|
-
`);
|
|
45582
|
-
return 0;
|
|
45583
46167
|
}
|
|
45584
46168
|
};
|
|
45585
46169
|
var RedactCommand = class extends Command {
|
|
@@ -45624,11 +46208,9 @@ var RedactStreamCommand = class extends Command {
|
|
|
45624
46208
|
static usage = Command.Usage({
|
|
45625
46209
|
description: "Stream stdin through the line-buffered redactor to stdout.",
|
|
45626
46210
|
details: `
|
|
45627
|
-
|
|
45628
|
-
|
|
45629
|
-
|
|
45630
|
-
line structure. Multi-line patterns are NOT applied here (see the
|
|
45631
|
-
streaming-mode limitation in @keynv/redactor).
|
|
46211
|
+
Pipe any output through the line-buffered redactor to stdout.
|
|
46212
|
+
Preserves original line structure. Multi-line patterns are NOT applied
|
|
46213
|
+
here (see the streaming-mode limitation in the redactor package).
|
|
45632
46214
|
`
|
|
45633
46215
|
});
|
|
45634
46216
|
async execute() {
|
|
@@ -45643,18 +46225,13 @@ streaming-mode limitation in @keynv/redactor).
|
|
|
45643
46225
|
// src/commands/secret.ts
|
|
45644
46226
|
init_dist();
|
|
45645
46227
|
init_http();
|
|
46228
|
+
init_secret();
|
|
45646
46229
|
init_format();
|
|
45647
|
-
init_tty();
|
|
45648
46230
|
init_cancel();
|
|
45649
46231
|
init_pickProject();
|
|
45650
46232
|
init_pickSecret();
|
|
45651
|
-
|
|
45652
|
-
|
|
45653
|
-
const data = await client.request("/v1/projects");
|
|
45654
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
45655
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
45656
|
-
return match.id;
|
|
45657
|
-
}
|
|
46233
|
+
init_tty();
|
|
46234
|
+
var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
|
|
45658
46235
|
function missingAlias(stderr) {
|
|
45659
46236
|
stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
|
|
45660
46237
|
return 1;
|
|
@@ -45685,45 +46262,50 @@ var SecretCreateCommand = class extends Command {
|
|
|
45685
46262
|
});
|
|
45686
46263
|
stdin = options_exports.Boolean("--stdin", false);
|
|
45687
46264
|
async execute() {
|
|
45688
|
-
|
|
45689
|
-
|
|
45690
|
-
|
|
45691
|
-
|
|
45692
|
-
|
|
45693
|
-
if (!
|
|
45694
|
-
|
|
45695
|
-
|
|
45696
|
-
|
|
45697
|
-
|
|
45698
|
-
|
|
45699
|
-
|
|
45700
|
-
|
|
45701
|
-
|
|
46265
|
+
try {
|
|
46266
|
+
const client = new ApiClient();
|
|
46267
|
+
await client.ensureHydrated();
|
|
46268
|
+
let alias2 = this.alias;
|
|
46269
|
+
let value = this.value;
|
|
46270
|
+
if (!alias2) {
|
|
46271
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46272
|
+
try {
|
|
46273
|
+
const built = await promptNewSecret(client);
|
|
46274
|
+
if (!built) return 1;
|
|
46275
|
+
alias2 = built.alias;
|
|
46276
|
+
value = built.value;
|
|
46277
|
+
} catch (err) {
|
|
46278
|
+
if (err instanceof UserCancelled) return 130;
|
|
46279
|
+
throw err;
|
|
46280
|
+
}
|
|
45702
46281
|
}
|
|
45703
|
-
|
|
45704
|
-
|
|
45705
|
-
|
|
45706
|
-
|
|
46282
|
+
const parsed = parseAlias(alias2);
|
|
46283
|
+
if (!parsed) {
|
|
46284
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46285
|
+
${ALIAS_FORMAT_HINT}
|
|
45707
46286
|
`);
|
|
45708
|
-
|
|
45709
|
-
}
|
|
45710
|
-
if (this.stdin) {
|
|
45711
|
-
const chunks = [];
|
|
45712
|
-
for await (const chunk of this.context.stdin) {
|
|
45713
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46287
|
+
return 1;
|
|
45714
46288
|
}
|
|
45715
|
-
|
|
45716
|
-
|
|
45717
|
-
|
|
45718
|
-
|
|
45719
|
-
|
|
45720
|
-
|
|
45721
|
-
|
|
45722
|
-
|
|
45723
|
-
|
|
45724
|
-
|
|
46289
|
+
if (this.stdin) {
|
|
46290
|
+
const chunks = [];
|
|
46291
|
+
for await (const chunk of this.context.stdin) {
|
|
46292
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46293
|
+
}
|
|
46294
|
+
value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
46295
|
+
} else if (value === void 0) {
|
|
46296
|
+
value = await promptHidden("value: ");
|
|
46297
|
+
}
|
|
46298
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46299
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
46300
|
+
method: "POST",
|
|
46301
|
+
body: { env: parsed.environment, key: parsed.key, value }
|
|
46302
|
+
});
|
|
46303
|
+
this.context.stdout.write(`created ${parsed.literal}
|
|
45725
46304
|
`);
|
|
45726
|
-
|
|
46305
|
+
return 0;
|
|
46306
|
+
} catch (err) {
|
|
46307
|
+
return handleExecError(this.context.stderr, err);
|
|
46308
|
+
}
|
|
45727
46309
|
}
|
|
45728
46310
|
};
|
|
45729
46311
|
var SecretGetCommand = class extends Command {
|
|
@@ -45734,39 +46316,44 @@ var SecretGetCommand = class extends Command {
|
|
|
45734
46316
|
alias = options_exports.String({ required: false });
|
|
45735
46317
|
json = options_exports.Boolean("--json", false);
|
|
45736
46318
|
async execute() {
|
|
45737
|
-
|
|
45738
|
-
|
|
45739
|
-
|
|
45740
|
-
|
|
45741
|
-
if (!
|
|
45742
|
-
|
|
45743
|
-
|
|
45744
|
-
|
|
45745
|
-
|
|
45746
|
-
|
|
46319
|
+
try {
|
|
46320
|
+
const client = new ApiClient();
|
|
46321
|
+
await client.ensureHydrated();
|
|
46322
|
+
let alias2 = this.alias;
|
|
46323
|
+
if (!alias2) {
|
|
46324
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46325
|
+
try {
|
|
46326
|
+
alias2 = await pickAliasInteractive(client) ?? void 0;
|
|
46327
|
+
} catch (err) {
|
|
46328
|
+
if (err instanceof UserCancelled) return 130;
|
|
46329
|
+
throw err;
|
|
46330
|
+
}
|
|
46331
|
+
if (!alias2) return 1;
|
|
45747
46332
|
}
|
|
45748
|
-
|
|
45749
|
-
|
|
45750
|
-
|
|
45751
|
-
|
|
45752
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46333
|
+
const parsed = parseAlias(alias2);
|
|
46334
|
+
if (!parsed) {
|
|
46335
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46336
|
+
${ALIAS_FORMAT_HINT}
|
|
45753
46337
|
`);
|
|
45754
|
-
|
|
45755
|
-
|
|
45756
|
-
|
|
45757
|
-
|
|
45758
|
-
|
|
45759
|
-
);
|
|
45760
|
-
if (this.json) {
|
|
45761
|
-
this.context.stdout.write(
|
|
45762
|
-
`${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
|
|
45763
|
-
`
|
|
46338
|
+
return 1;
|
|
46339
|
+
}
|
|
46340
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46341
|
+
const data = await client.request(
|
|
46342
|
+
`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
|
|
45764
46343
|
);
|
|
45765
|
-
|
|
45766
|
-
|
|
46344
|
+
if (this.json) {
|
|
46345
|
+
this.context.stdout.write(
|
|
46346
|
+
`${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
|
|
46347
|
+
`
|
|
46348
|
+
);
|
|
46349
|
+
} else {
|
|
46350
|
+
this.context.stdout.write(`${data.value}
|
|
45767
46351
|
`);
|
|
46352
|
+
}
|
|
46353
|
+
return 0;
|
|
46354
|
+
} catch (err) {
|
|
46355
|
+
return handleExecError(this.context.stderr, err);
|
|
45768
46356
|
}
|
|
45769
|
-
return 0;
|
|
45770
46357
|
}
|
|
45771
46358
|
};
|
|
45772
46359
|
var SecretListCommand = class extends Command {
|
|
@@ -45777,42 +46364,47 @@ var SecretListCommand = class extends Command {
|
|
|
45777
46364
|
project = options_exports.String({ required: false });
|
|
45778
46365
|
json = options_exports.Boolean("--json", false);
|
|
45779
46366
|
async execute() {
|
|
45780
|
-
|
|
45781
|
-
|
|
45782
|
-
|
|
45783
|
-
|
|
45784
|
-
if (!
|
|
45785
|
-
|
|
45786
|
-
|
|
45787
|
-
|
|
45788
|
-
|
|
45789
|
-
|
|
45790
|
-
|
|
45791
|
-
|
|
45792
|
-
|
|
45793
|
-
|
|
45794
|
-
|
|
46367
|
+
try {
|
|
46368
|
+
const client = new ApiClient();
|
|
46369
|
+
await client.ensureHydrated();
|
|
46370
|
+
let projectName = this.project;
|
|
46371
|
+
if (!projectName) {
|
|
46372
|
+
if (!isInteractive()) {
|
|
46373
|
+
this.context.stderr.write("keynv: missing <project>.\n");
|
|
46374
|
+
return 1;
|
|
46375
|
+
}
|
|
46376
|
+
try {
|
|
46377
|
+
const picked = await pickProject(client, "Project");
|
|
46378
|
+
if (!picked) return 1;
|
|
46379
|
+
projectName = picked.name;
|
|
46380
|
+
} catch (err) {
|
|
46381
|
+
if (err instanceof UserCancelled) return 130;
|
|
46382
|
+
throw err;
|
|
46383
|
+
}
|
|
45795
46384
|
}
|
|
45796
|
-
|
|
45797
|
-
|
|
45798
|
-
|
|
45799
|
-
|
|
45800
|
-
|
|
46385
|
+
const resolvedProjectName = projectName.startsWith("@") ? projectName.slice(1).split(".")[0] ?? projectName : projectName;
|
|
46386
|
+
const projectId2 = await resolveProjectId(client, resolvedProjectName);
|
|
46387
|
+
const data = await client.request(`/v1/projects/${projectId2}/secrets`);
|
|
46388
|
+
if (this.json) {
|
|
46389
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45801
46390
|
`);
|
|
46391
|
+
return 0;
|
|
46392
|
+
}
|
|
46393
|
+
if (data.secrets.length === 0) {
|
|
46394
|
+
this.context.stdout.write("no secrets\n");
|
|
46395
|
+
return 0;
|
|
46396
|
+
}
|
|
46397
|
+
this.context.stdout.write(
|
|
46398
|
+
`${table(
|
|
46399
|
+
["alias", "version", "created_at"],
|
|
46400
|
+
data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
|
|
46401
|
+
)}
|
|
46402
|
+
`
|
|
46403
|
+
);
|
|
45802
46404
|
return 0;
|
|
46405
|
+
} catch (err) {
|
|
46406
|
+
return handleExecError(this.context.stderr, err);
|
|
45803
46407
|
}
|
|
45804
|
-
if (data.secrets.length === 0) {
|
|
45805
|
-
this.context.stdout.write("no secrets\n");
|
|
45806
|
-
return 0;
|
|
45807
|
-
}
|
|
45808
|
-
this.context.stdout.write(
|
|
45809
|
-
`${table(
|
|
45810
|
-
["alias", "version", "created_at"],
|
|
45811
|
-
data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
|
|
45812
|
-
)}
|
|
45813
|
-
`
|
|
45814
|
-
);
|
|
45815
|
-
return 0;
|
|
45816
46408
|
}
|
|
45817
46409
|
};
|
|
45818
46410
|
var SecretRotateCommand = class extends Command {
|
|
@@ -45822,45 +46414,50 @@ var SecretRotateCommand = class extends Command {
|
|
|
45822
46414
|
value = options_exports.String("--value");
|
|
45823
46415
|
stdin = options_exports.Boolean("--stdin", false);
|
|
45824
46416
|
async execute() {
|
|
45825
|
-
|
|
45826
|
-
|
|
45827
|
-
|
|
45828
|
-
|
|
45829
|
-
if (!
|
|
45830
|
-
|
|
45831
|
-
|
|
45832
|
-
|
|
45833
|
-
|
|
45834
|
-
|
|
46417
|
+
try {
|
|
46418
|
+
const client = new ApiClient();
|
|
46419
|
+
await client.ensureHydrated();
|
|
46420
|
+
let alias2 = this.alias;
|
|
46421
|
+
if (!alias2) {
|
|
46422
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46423
|
+
try {
|
|
46424
|
+
alias2 = await pickAliasInteractive(client) ?? void 0;
|
|
46425
|
+
} catch (err) {
|
|
46426
|
+
if (err instanceof UserCancelled) return 130;
|
|
46427
|
+
throw err;
|
|
46428
|
+
}
|
|
46429
|
+
if (!alias2) return 1;
|
|
45835
46430
|
}
|
|
45836
|
-
|
|
45837
|
-
|
|
45838
|
-
|
|
45839
|
-
|
|
45840
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46431
|
+
const parsed = parseAlias(alias2);
|
|
46432
|
+
if (!parsed) {
|
|
46433
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46434
|
+
${ALIAS_FORMAT_HINT}
|
|
45841
46435
|
`);
|
|
45842
|
-
|
|
45843
|
-
}
|
|
45844
|
-
let value;
|
|
45845
|
-
if (this.stdin) {
|
|
45846
|
-
const chunks = [];
|
|
45847
|
-
for await (const chunk of this.context.stdin) {
|
|
45848
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46436
|
+
return 1;
|
|
45849
46437
|
}
|
|
45850
|
-
value
|
|
45851
|
-
|
|
45852
|
-
|
|
45853
|
-
|
|
45854
|
-
|
|
45855
|
-
|
|
45856
|
-
|
|
45857
|
-
|
|
45858
|
-
|
|
45859
|
-
|
|
45860
|
-
|
|
45861
|
-
|
|
46438
|
+
let value;
|
|
46439
|
+
if (this.stdin) {
|
|
46440
|
+
const chunks = [];
|
|
46441
|
+
for await (const chunk of this.context.stdin) {
|
|
46442
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46443
|
+
}
|
|
46444
|
+
value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
46445
|
+
} else if (this.value !== void 0) {
|
|
46446
|
+
value = this.value;
|
|
46447
|
+
} else {
|
|
46448
|
+
value = await promptHidden("new value: ");
|
|
46449
|
+
}
|
|
46450
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46451
|
+
const data = await client.request(
|
|
46452
|
+
`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
|
|
46453
|
+
{ method: "POST", body: { new_value: value } }
|
|
46454
|
+
);
|
|
46455
|
+
this.context.stdout.write(`rotated ${data.alias} \u2192 v${data.version}
|
|
45862
46456
|
`);
|
|
45863
|
-
|
|
46457
|
+
return 0;
|
|
46458
|
+
} catch (err) {
|
|
46459
|
+
return handleExecError(this.context.stderr, err);
|
|
46460
|
+
}
|
|
45864
46461
|
}
|
|
45865
46462
|
};
|
|
45866
46463
|
var SecretDeleteCommand = class extends Command {
|
|
@@ -45868,31 +46465,170 @@ var SecretDeleteCommand = class extends Command {
|
|
|
45868
46465
|
static usage = Command.Usage({ description: "Soft-delete a secret." });
|
|
45869
46466
|
alias = options_exports.String({ required: false });
|
|
45870
46467
|
async execute() {
|
|
45871
|
-
|
|
45872
|
-
|
|
45873
|
-
|
|
45874
|
-
|
|
45875
|
-
if (!
|
|
45876
|
-
|
|
45877
|
-
|
|
45878
|
-
|
|
45879
|
-
|
|
45880
|
-
|
|
46468
|
+
try {
|
|
46469
|
+
const client = new ApiClient();
|
|
46470
|
+
await client.ensureHydrated();
|
|
46471
|
+
let alias2 = this.alias;
|
|
46472
|
+
if (!alias2) {
|
|
46473
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46474
|
+
try {
|
|
46475
|
+
alias2 = await pickAliasInteractive(client) ?? void 0;
|
|
46476
|
+
} catch (err) {
|
|
46477
|
+
if (err instanceof UserCancelled) return 130;
|
|
46478
|
+
throw err;
|
|
46479
|
+
}
|
|
46480
|
+
if (!alias2) return 1;
|
|
45881
46481
|
}
|
|
45882
|
-
|
|
45883
|
-
|
|
45884
|
-
|
|
45885
|
-
|
|
45886
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46482
|
+
const parsed = parseAlias(alias2);
|
|
46483
|
+
if (!parsed) {
|
|
46484
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46485
|
+
${ALIAS_FORMAT_HINT}
|
|
45887
46486
|
`);
|
|
45888
|
-
|
|
46487
|
+
return 1;
|
|
46488
|
+
}
|
|
46489
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46490
|
+
await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
|
|
46491
|
+
method: "DELETE"
|
|
46492
|
+
});
|
|
46493
|
+
this.context.stdout.write(`deleted ${parsed.literal}
|
|
46494
|
+
`);
|
|
46495
|
+
return 0;
|
|
46496
|
+
} catch (err) {
|
|
46497
|
+
return handleExecError(this.context.stderr, err);
|
|
45889
46498
|
}
|
|
45890
|
-
|
|
45891
|
-
|
|
45892
|
-
|
|
45893
|
-
|
|
45894
|
-
|
|
46499
|
+
}
|
|
46500
|
+
};
|
|
46501
|
+
function generateSecret(bytes = 32) {
|
|
46502
|
+
return randomBytes(bytes).toString("base64");
|
|
46503
|
+
}
|
|
46504
|
+
var ServerInitCommand = class extends Command {
|
|
46505
|
+
static paths = [["server", "init"]];
|
|
46506
|
+
static usage = Command.Usage({
|
|
46507
|
+
description: "Generate secrets and deployment files for a new keynv server.",
|
|
46508
|
+
details: `
|
|
46509
|
+
Interactive onboarding wizard that generates the configuration
|
|
46510
|
+
a fresh keynv server needs: JWT signing secret, master encryption
|
|
46511
|
+
key, and deployment templates.
|
|
46512
|
+
|
|
46513
|
+
Outputs:
|
|
46514
|
+
\u2022 A .keynv-server.env file you feed into your deployment
|
|
46515
|
+
(Docker Compose, Coolify, or bare-metal).
|
|
46516
|
+
\u2022 Optionally a docker-compose.yml in the current directory.
|
|
46517
|
+
|
|
46518
|
+
Non-interactive with --yes to skip prompts.
|
|
46519
|
+
`,
|
|
46520
|
+
examples: [
|
|
46521
|
+
["Interactive wizard", "$0 server init"],
|
|
46522
|
+
["Non-interactive (CI/scripts)", "$0 server init --yes"],
|
|
46523
|
+
["Docker Compose output", "$0 server init --compose"],
|
|
46524
|
+
["Custom output path", "$0 server init --out ./config/env"]
|
|
46525
|
+
]
|
|
46526
|
+
});
|
|
46527
|
+
yes = options_exports.Boolean("--yes", false, {
|
|
46528
|
+
description: "Skip all prompts (non-interactive)."
|
|
46529
|
+
});
|
|
46530
|
+
compose = options_exports.Boolean("--compose", false, {
|
|
46531
|
+
description: "Also write a docker-compose.yml to the current directory."
|
|
46532
|
+
});
|
|
46533
|
+
out = options_exports.String("--out", {
|
|
46534
|
+
description: "Write server env file to this path (default: .keynv-server.env)."
|
|
46535
|
+
});
|
|
46536
|
+
async execute() {
|
|
46537
|
+
const jwtSecret = generateSecret(48);
|
|
46538
|
+
const masterKey = generateSecret(32);
|
|
46539
|
+
const webSessionSecret = generateSecret(48);
|
|
46540
|
+
const outPath = this.out ?? ".keynv-server.env";
|
|
46541
|
+
const contents = [
|
|
46542
|
+
"# Generated by `keynv server init`.",
|
|
46543
|
+
"# Keep this file secret \u2014 it contains cryptographic key material.",
|
|
46544
|
+
`KEYNV_JWT_SECRET=${jwtSecret}`,
|
|
46545
|
+
`KEYNV_MASTER_KEY=${masterKey}`,
|
|
46546
|
+
"",
|
|
46547
|
+
"# Web dashboard session secret (required for Next.js JWE session tokens).",
|
|
46548
|
+
`KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
|
|
46549
|
+
"",
|
|
46550
|
+
"# Server configuration (uncomment and adjust):",
|
|
46551
|
+
"# KEYNV_PORT=8080",
|
|
46552
|
+
"# KEYNV_WEB_URL=https://keynv.example.com",
|
|
46553
|
+
"# KEYNV_PUBLIC_REGISTRATION=false",
|
|
46554
|
+
"# KEYNV_DB_PATH=./keynv.db",
|
|
46555
|
+
"# KEYNV_MASTER_KEY_FILE=./master.key",
|
|
46556
|
+
"# KEYNV_RATE_LIMIT_PER_MINUTE=120",
|
|
46557
|
+
"# KEYNV_ARGON2_MEMORY_KIB=46080",
|
|
46558
|
+
"# KEYNV_ARGON2_TIME_COST=3"
|
|
46559
|
+
].join("\n");
|
|
46560
|
+
const { writeFileSync: writeFileSync4 } = await import('fs');
|
|
46561
|
+
writeFileSync4(outPath, `${contents}
|
|
45895
46562
|
`);
|
|
46563
|
+
this.context.stdout.write(`Wrote ${outPath}
|
|
46564
|
+
|
|
46565
|
+
`);
|
|
46566
|
+
this.context.stdout.write("Generated secrets:\n");
|
|
46567
|
+
this.context.stdout.write(
|
|
46568
|
+
` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
46569
|
+
`
|
|
46570
|
+
);
|
|
46571
|
+
this.context.stdout.write(
|
|
46572
|
+
` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
|
|
46573
|
+
`
|
|
46574
|
+
);
|
|
46575
|
+
this.context.stdout.write(
|
|
46576
|
+
` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
46577
|
+
`
|
|
46578
|
+
);
|
|
46579
|
+
this.context.stdout.write("\nNext steps:\n");
|
|
46580
|
+
this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
|
|
46581
|
+
`);
|
|
46582
|
+
this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
|
|
46583
|
+
this.context.stdout.write(" or load it into Coolify as environment variables.\n");
|
|
46584
|
+
this.context.stdout.write(" 2. Bootstrap the first owner:\n");
|
|
46585
|
+
this.context.stdout.write(
|
|
46586
|
+
" docker exec -it <server-container> node dist/bootstrap.js \\\n"
|
|
46587
|
+
);
|
|
46588
|
+
this.context.stdout.write(" --owner-email alice@example.com \\\n");
|
|
46589
|
+
this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
|
|
46590
|
+
this.context.stdout.write(' --org-name "Acme Inc"\n');
|
|
46591
|
+
this.context.stdout.write(" 3. Install the CLI and log in:\n");
|
|
46592
|
+
this.context.stdout.write(" npm install -g @keynv/cli\n");
|
|
46593
|
+
this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
|
|
46594
|
+
this.context.stdout.write(" 4. Onboard your first project:\n");
|
|
46595
|
+
this.context.stdout.write(" cd your-project && keynv init\n");
|
|
46596
|
+
if (this.compose) {
|
|
46597
|
+
const composeContent = `# keynv server + Litestream sidecar
|
|
46598
|
+
# Generated by \`keynv server init\`.
|
|
46599
|
+
# Load with: docker compose --env-file .keynv-server.env up -d
|
|
46600
|
+
|
|
46601
|
+
services:
|
|
46602
|
+
server:
|
|
46603
|
+
image: ghcr.io/keynv-labs/keynv-server:latest
|
|
46604
|
+
restart: unless-stopped
|
|
46605
|
+
ports:
|
|
46606
|
+
- '8080:8080'
|
|
46607
|
+
env_file:
|
|
46608
|
+
- .keynv-server.env
|
|
46609
|
+
volumes:
|
|
46610
|
+
- keynv-data:/data
|
|
46611
|
+
|
|
46612
|
+
litestream:
|
|
46613
|
+
image: litestream/litestream:latest
|
|
46614
|
+
restart: unless-stopped
|
|
46615
|
+
volumes:
|
|
46616
|
+
- keynv-data:/data
|
|
46617
|
+
command: replicate
|
|
46618
|
+
environment:
|
|
46619
|
+
LITESTREAM_DB_PATH: /data/keynv.db
|
|
46620
|
+
LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
|
|
46621
|
+
AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
|
|
46622
|
+
AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
|
|
46623
|
+
|
|
46624
|
+
volumes:
|
|
46625
|
+
keynv-data:
|
|
46626
|
+
`;
|
|
46627
|
+
writeFileSync4("docker-compose.yml", composeContent);
|
|
46628
|
+
this.context.stdout.write(
|
|
46629
|
+
"\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
|
|
46630
|
+
);
|
|
46631
|
+
}
|
|
45896
46632
|
return 0;
|
|
45897
46633
|
}
|
|
45898
46634
|
};
|
|
@@ -46248,18 +46984,9 @@ var TESTERS = [
|
|
|
46248
46984
|
sshTester,
|
|
46249
46985
|
httpTester
|
|
46250
46986
|
];
|
|
46251
|
-
function findTester(type) {
|
|
46252
|
-
return TESTERS.find((t) => t.type === type) ?? null;
|
|
46253
|
-
}
|
|
46254
46987
|
|
|
46255
46988
|
// src/commands/test.ts
|
|
46256
46989
|
init_http();
|
|
46257
|
-
async function findProjectIdByName3(client, name) {
|
|
46258
|
-
const data = await client.request("/v1/projects");
|
|
46259
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
46260
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
46261
|
-
return match.id;
|
|
46262
|
-
}
|
|
46263
46990
|
function parseTargets(specs) {
|
|
46264
46991
|
const out = {};
|
|
46265
46992
|
for (const spec of specs) {
|
|
@@ -46313,12 +47040,17 @@ the error message.
|
|
|
46313
47040
|
);
|
|
46314
47041
|
return 2;
|
|
46315
47042
|
}
|
|
46316
|
-
const tester =
|
|
47043
|
+
const tester = TESTERS.find((t) => t.type === this.as);
|
|
46317
47044
|
if (!tester) {
|
|
46318
47045
|
this.context.stderr.write(`keynv: unknown tester '${this.as}'
|
|
46319
47046
|
`);
|
|
46320
47047
|
return 1;
|
|
46321
47048
|
}
|
|
47049
|
+
const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
|
|
47050
|
+
if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
|
|
47051
|
+
this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
|
|
47052
|
+
return 2;
|
|
47053
|
+
}
|
|
46322
47054
|
let target;
|
|
46323
47055
|
try {
|
|
46324
47056
|
target = parseTargets(this.targets ?? []);
|
|
@@ -46335,7 +47067,7 @@ the error message.
|
|
|
46335
47067
|
}
|
|
46336
47068
|
let value;
|
|
46337
47069
|
try {
|
|
46338
|
-
const projectId2 = await
|
|
47070
|
+
const projectId2 = await resolveProjectId(client, parsedAlias.project);
|
|
46339
47071
|
const data = await client.request(
|
|
46340
47072
|
`/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
|
|
46341
47073
|
);
|
|
@@ -46346,11 +47078,6 @@ the error message.
|
|
|
46346
47078
|
`);
|
|
46347
47079
|
return 1;
|
|
46348
47080
|
}
|
|
46349
|
-
const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
|
|
46350
|
-
if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
|
|
46351
|
-
this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
|
|
46352
|
-
return 2;
|
|
46353
|
-
}
|
|
46354
47081
|
const result = await runTest({
|
|
46355
47082
|
tester,
|
|
46356
47083
|
secret: { alias: parsedAlias.literal, value },
|
|
@@ -46392,25 +47119,6 @@ the error message.
|
|
|
46392
47119
|
}
|
|
46393
47120
|
};
|
|
46394
47121
|
|
|
46395
|
-
// src/commands/ui.ts
|
|
46396
|
-
init_menu();
|
|
46397
|
-
init_tty();
|
|
46398
|
-
var UICommand = class extends Command {
|
|
46399
|
-
static paths = [["ui"]];
|
|
46400
|
-
static usage = Command.Usage({
|
|
46401
|
-
description: "Open the interactive menu (also runs by default when no args are given)."
|
|
46402
|
-
});
|
|
46403
|
-
async execute() {
|
|
46404
|
-
if (!isInteractive()) {
|
|
46405
|
-
this.context.stdout.write(
|
|
46406
|
-
"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"
|
|
46407
|
-
);
|
|
46408
|
-
return 0;
|
|
46409
|
-
}
|
|
46410
|
-
return runMenu();
|
|
46411
|
-
}
|
|
46412
|
-
};
|
|
46413
|
-
|
|
46414
47122
|
// src/index.ts
|
|
46415
47123
|
init_format();
|
|
46416
47124
|
init_version();
|
|
@@ -46444,7 +47152,7 @@ cli.register(InitCommand);
|
|
|
46444
47152
|
cli.register(RedactCommand);
|
|
46445
47153
|
cli.register(RedactStreamCommand);
|
|
46446
47154
|
cli.register(TestCommand);
|
|
46447
|
-
cli.register(
|
|
47155
|
+
cli.register(ServerInitCommand);
|
|
46448
47156
|
var argv = process.argv.slice(2);
|
|
46449
47157
|
if (argv.length === 0) {
|
|
46450
47158
|
const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));
|