@thisisamank/noenv-cli 0.1.1 → 0.2.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/CHANGELOG.md +31 -0
- package/dist/noenv.js +757 -327
- package/package.json +3 -2
package/dist/noenv.js
CHANGED
|
@@ -1,16 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// ../packages/config/src/names.ts
|
|
4
|
+
var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
5
|
+
var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
|
+
var REPO = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)+$/;
|
|
7
|
+
|
|
8
|
+
// src/deployment.ts
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
// src/files.ts
|
|
13
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
async function readJsonFile(path) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === "ENOENT") return null;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function writeJsonFile(path, value, flag = "w") {
|
|
24
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
25
|
+
await writeFile(path, JSON.stringify(value), { mode: 384, flag });
|
|
26
|
+
}
|
|
27
|
+
async function removeFile(path) {
|
|
28
|
+
await rm(path, { force: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/deployment.ts
|
|
32
|
+
function configDir(env = process.env) {
|
|
33
|
+
return env.NOENV_CONFIG_DIR ?? join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "noenv");
|
|
34
|
+
}
|
|
35
|
+
function deploymentAt(value, env = process.env, account) {
|
|
36
|
+
const url = URL.parse(value);
|
|
37
|
+
if (!url || url.protocol !== "https:" && url.protocol !== "http:") {
|
|
38
|
+
throw new Error(`${value} is not an http or https URL`);
|
|
39
|
+
}
|
|
40
|
+
return { url: url.origin, dir: join(configDir(env), url.host.replace(":", "_")), fetch: globalThis.fetch, account };
|
|
41
|
+
}
|
|
42
|
+
async function readAccounts(env = process.env) {
|
|
43
|
+
return await readJsonFile(accountsPath(env)) ?? { accounts: {} };
|
|
44
|
+
}
|
|
45
|
+
async function writeAccounts(accounts, env = process.env) {
|
|
46
|
+
await writeJsonFile(accountsPath(env), accounts);
|
|
47
|
+
}
|
|
48
|
+
function matchAccount(accounts, repo) {
|
|
49
|
+
let best;
|
|
50
|
+
for (const [name2, account] of Object.entries(accounts.accounts)) {
|
|
51
|
+
for (const prefix of account.match) {
|
|
52
|
+
const clean = prefix.toLowerCase().replace(/\/+$/, "");
|
|
53
|
+
if ((repo === clean || repo.startsWith(`${clean}/`)) && clean.length > (best?.length ?? -1)) {
|
|
54
|
+
best = { name: name2, length: clean.length };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return best?.name;
|
|
59
|
+
}
|
|
60
|
+
async function resolveDeployment(repo, env = process.env) {
|
|
61
|
+
const accounts = await readAccounts(env);
|
|
62
|
+
const name2 = env.NOENV_ACCOUNT || (repo ? matchAccount(accounts, repo) : void 0) || accounts.default;
|
|
63
|
+
if (name2) {
|
|
64
|
+
const account = accounts.accounts[name2];
|
|
65
|
+
if (!account) throw new Error(`there is no account named "${name2}", run \`noenv account list\``);
|
|
66
|
+
return deploymentAt(account.url, env, name2);
|
|
67
|
+
}
|
|
68
|
+
return env.NOENV_URL ? deploymentAt(env.NOENV_URL, env) : null;
|
|
69
|
+
}
|
|
70
|
+
function accountsPath(env) {
|
|
71
|
+
return join(configDir(env), "accounts.json");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/session.ts
|
|
75
|
+
import { spawn } from "node:child_process";
|
|
76
|
+
import { hostname } from "node:os";
|
|
77
|
+
import { join as join3 } from "node:path";
|
|
78
|
+
|
|
3
79
|
// package.json
|
|
4
80
|
var package_default = {
|
|
5
81
|
name: "@thisisamank/noenv-cli",
|
|
6
|
-
version: "0.
|
|
82
|
+
version: "0.2.0",
|
|
7
83
|
description: "Encrypted environment variables that load when you cd into a repo. CLI for noenv.",
|
|
8
84
|
type: "module",
|
|
9
85
|
bin: {
|
|
10
86
|
noenv: "dist/noenv.js"
|
|
11
87
|
},
|
|
12
88
|
files: [
|
|
13
|
-
"dist"
|
|
89
|
+
"dist",
|
|
90
|
+
"CHANGELOG.md"
|
|
14
91
|
],
|
|
15
92
|
engines: {
|
|
16
93
|
node: ">=22"
|
|
@@ -87,19 +164,6 @@ function environmentPath(repo, environment) {
|
|
|
87
164
|
return `${repoPath(repo)}/environments/${environment}`;
|
|
88
165
|
}
|
|
89
166
|
|
|
90
|
-
// src/deployment.ts
|
|
91
|
-
import { homedir } from "node:os";
|
|
92
|
-
import { join } from "node:path";
|
|
93
|
-
function currentDeployment(env = process.env) {
|
|
94
|
-
if (!env.NOENV_URL) return null;
|
|
95
|
-
const url = URL.parse(env.NOENV_URL);
|
|
96
|
-
if (!url || url.protocol !== "https:" && url.protocol !== "http:") {
|
|
97
|
-
throw new Error("NOENV_URL must be an http or https URL");
|
|
98
|
-
}
|
|
99
|
-
const base = env.NOENV_CONFIG_DIR ?? join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "noenv");
|
|
100
|
-
return { url: url.origin, dir: join(base, url.host.replace(":", "_")), fetch: globalThis.fetch };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
167
|
// src/identity.ts
|
|
104
168
|
import { join as join2 } from "node:path";
|
|
105
169
|
|
|
@@ -206,25 +270,6 @@ async function unwrapPrivateKey(wrapped, recipientPrivateKey) {
|
|
|
206
270
|
return crypto.subtle.importKey("pkcs8", pkcs8, ECDH, true, ["deriveBits"]);
|
|
207
271
|
}
|
|
208
272
|
|
|
209
|
-
// src/files.ts
|
|
210
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
211
|
-
import { dirname } from "node:path";
|
|
212
|
-
async function readJsonFile(path) {
|
|
213
|
-
try {
|
|
214
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
215
|
-
} catch (error) {
|
|
216
|
-
if (error.code === "ENOENT") return null;
|
|
217
|
-
throw error;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function writeJsonFile(path, value, flag = "w") {
|
|
221
|
-
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
222
|
-
await writeFile(path, JSON.stringify(value), { mode: 384, flag });
|
|
223
|
-
}
|
|
224
|
-
async function removeFile(path) {
|
|
225
|
-
await rm(path, { force: true });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
273
|
// src/identity.ts
|
|
229
274
|
async function loadIdentity(deployment) {
|
|
230
275
|
const stored = await readJsonFile(identityPath(deployment));
|
|
@@ -256,9 +301,6 @@ function identityPath(deployment) {
|
|
|
256
301
|
}
|
|
257
302
|
|
|
258
303
|
// src/session.ts
|
|
259
|
-
import { spawn } from "node:child_process";
|
|
260
|
-
import { hostname } from "node:os";
|
|
261
|
-
import { join as join3 } from "node:path";
|
|
262
304
|
async function loadSession(deployment) {
|
|
263
305
|
const session = await readJsonFile(sessionPath(deployment));
|
|
264
306
|
if (!session) return null;
|
|
@@ -270,7 +312,7 @@ async function clearSession(deployment) {
|
|
|
270
312
|
async function requireSession(deployment) {
|
|
271
313
|
const token = await loadSession(deployment);
|
|
272
314
|
if (!token) {
|
|
273
|
-
throw new Error(
|
|
315
|
+
throw new Error(`you are not signed in to ${deployment.account ?? deployment.url}, open a new shell or run \`noenv auth${deployment.account ? ` ${deployment.account}` : ""}\``);
|
|
274
316
|
}
|
|
275
317
|
return token;
|
|
276
318
|
}
|
|
@@ -325,8 +367,8 @@ function openBrowser(url) {
|
|
|
325
367
|
spawn(command2, [url], { detached: true, stdio: "ignore" }).on("error", () => {
|
|
326
368
|
}).unref();
|
|
327
369
|
}
|
|
328
|
-
function startBackgroundPoller() {
|
|
329
|
-
spawn(process.execPath, [...process.execArgv, process.argv[1], "auth", "--wait"], {
|
|
370
|
+
function startBackgroundPoller(deployment) {
|
|
371
|
+
spawn(process.execPath, [...process.execArgv, process.argv[1], "auth", "--wait", deployment.url], {
|
|
330
372
|
detached: true,
|
|
331
373
|
stdio: "ignore"
|
|
332
374
|
}).unref();
|
|
@@ -338,7 +380,67 @@ function pendingPath(deployment) {
|
|
|
338
380
|
return join3(deployment.dir, "pending-sign-in.json");
|
|
339
381
|
}
|
|
340
382
|
function sleep(ms) {
|
|
341
|
-
return new Promise((
|
|
383
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/commands/account.ts
|
|
387
|
+
var USAGE = "usage: noenv account add NAME URL [REPO_PREFIX...] | list | default NAME | remove NAME";
|
|
388
|
+
var PREFIX = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)*\/?$/;
|
|
389
|
+
async function accountCommand([action, ...args2]) {
|
|
390
|
+
const handlers = {
|
|
391
|
+
add: addAccount,
|
|
392
|
+
list: listAccounts,
|
|
393
|
+
default: setDefault,
|
|
394
|
+
remove: removeAccount
|
|
395
|
+
};
|
|
396
|
+
const handler = handlers[action ?? ""];
|
|
397
|
+
if (!handler) throw new Error(USAGE);
|
|
398
|
+
await handler(args2);
|
|
399
|
+
}
|
|
400
|
+
async function addAccount([name2, url, ...match]) {
|
|
401
|
+
if (!name2 || !url) throw new Error(USAGE);
|
|
402
|
+
if (!IDENTIFIER.test(name2)) throw new Error('the account name must contain only letters, digits, "_" or "-"');
|
|
403
|
+
const deployment = deploymentAt(url);
|
|
404
|
+
const prefixes = match.map((prefix) => prefix.toLowerCase());
|
|
405
|
+
const invalid = prefixes.find((prefix) => !PREFIX.test(prefix));
|
|
406
|
+
if (invalid) throw new Error(`${invalid} is not a repository prefix such as github.com/acme`);
|
|
407
|
+
const accounts = await readAccounts();
|
|
408
|
+
const clash = Object.entries(accounts.accounts).find(([other, account]) => other !== name2 && deploymentAt(account.url).url === deployment.url);
|
|
409
|
+
if (clash) throw new Error(`the account "${clash[0]}" already uses ${deployment.url}`);
|
|
410
|
+
accounts.accounts[name2] = { url: deployment.url, match: prefixes };
|
|
411
|
+
accounts.default ??= name2;
|
|
412
|
+
await writeAccounts(accounts);
|
|
413
|
+
console.log(`Saved account ${name2} (${deployment.url})${accounts.default === name2 ? " as the default" : ""}`);
|
|
414
|
+
if (!await loadSession(deployment)) console.log(`Run \`noenv auth ${name2}\` to sign in`);
|
|
415
|
+
}
|
|
416
|
+
async function listAccounts() {
|
|
417
|
+
const accounts = await readAccounts();
|
|
418
|
+
const entries = Object.entries(accounts.accounts);
|
|
419
|
+
if (entries.length === 0) {
|
|
420
|
+
console.log("No accounts. Add one with `noenv account add NAME URL [REPO_PREFIX...]`");
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
for (const [name2, account] of entries) {
|
|
424
|
+
const signedIn2 = await loadSession(deploymentAt(account.url)) ? "signed in" : "signed out";
|
|
425
|
+
const marker = accounts.default === name2 ? "*" : " ";
|
|
426
|
+
const match = account.match.length > 0 ? ` ${account.match.join(", ")}` : "";
|
|
427
|
+
console.log(`${marker} ${name2} ${account.url} ${signedIn2}${match}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function setDefault([name2]) {
|
|
431
|
+
const accounts = await readAccounts();
|
|
432
|
+
if (!name2 || !accounts.accounts[name2]) throw new Error(`there is no account named "${name2 ?? ""}"`);
|
|
433
|
+
accounts.default = name2;
|
|
434
|
+
await writeAccounts(accounts);
|
|
435
|
+
console.log(`${name2} is now the default account`);
|
|
436
|
+
}
|
|
437
|
+
async function removeAccount([name2]) {
|
|
438
|
+
const accounts = await readAccounts();
|
|
439
|
+
if (!name2 || !accounts.accounts[name2]) throw new Error(`there is no account named "${name2 ?? ""}"`);
|
|
440
|
+
delete accounts.accounts[name2];
|
|
441
|
+
if (accounts.default === name2) delete accounts.default;
|
|
442
|
+
await writeAccounts(accounts);
|
|
443
|
+
console.log(`Removed account ${name2}. Its key stays on this machine, so adding it again keeps your access.`);
|
|
342
444
|
}
|
|
343
445
|
|
|
344
446
|
// ../packages/config/src/errors.ts
|
|
@@ -1103,15 +1205,11 @@ function parse(toml, options = {}) {
|
|
|
1103
1205
|
// ../node_modules/smol-toml/dist/stringify.js
|
|
1104
1206
|
var HAS_WELLFORMED = !!"".isWellFormed;
|
|
1105
1207
|
|
|
1106
|
-
// ../packages/config/src/names.ts
|
|
1107
|
-
var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
1108
|
-
var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1109
|
-
var REPO = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)+$/;
|
|
1110
|
-
|
|
1111
1208
|
// ../packages/config/src/parse.ts
|
|
1112
1209
|
var DEFAULT_CONFIG = {
|
|
1113
1210
|
environment: "development",
|
|
1114
|
-
requiredSecrets: []
|
|
1211
|
+
requiredSecrets: [],
|
|
1212
|
+
wrangler: { secrets: [] }
|
|
1115
1213
|
};
|
|
1116
1214
|
function parseConfig(text3) {
|
|
1117
1215
|
const raw = parse(text3);
|
|
@@ -1127,7 +1225,24 @@ function parseConfig(text3) {
|
|
|
1127
1225
|
if (!Array.isArray(required) || !required.every(isEnvVarName)) {
|
|
1128
1226
|
throw new ConfigError("secrets.required must be a list of valid variable names");
|
|
1129
1227
|
}
|
|
1130
|
-
return { environment, requiredSecrets: required };
|
|
1228
|
+
return { environment, requiredSecrets: required, wrangler: parseWrangler(raw.wrangler) };
|
|
1229
|
+
}
|
|
1230
|
+
function parseWrangler(value) {
|
|
1231
|
+
const raw = value ?? {};
|
|
1232
|
+
const secrets = raw.secrets ?? [];
|
|
1233
|
+
if (!Array.isArray(secrets) || !secrets.every(isEnvVarName)) {
|
|
1234
|
+
throw new ConfigError("wrangler.secrets must be a list of valid variable names");
|
|
1235
|
+
}
|
|
1236
|
+
const config = { secrets };
|
|
1237
|
+
for (const key of ["environment", "env"]) {
|
|
1238
|
+
const field = raw[key];
|
|
1239
|
+
if (field === void 0) continue;
|
|
1240
|
+
if (typeof field !== "string" || !IDENTIFIER.test(field)) {
|
|
1241
|
+
throw new ConfigError(`wrangler.${key} must contain only letters, digits, "_" or "-"`);
|
|
1242
|
+
}
|
|
1243
|
+
config[key] = field;
|
|
1244
|
+
}
|
|
1245
|
+
return config;
|
|
1131
1246
|
}
|
|
1132
1247
|
function isEnvVarName(value) {
|
|
1133
1248
|
return typeof value === "string" && ENV_VAR_NAME.test(value);
|
|
@@ -1167,8 +1282,7 @@ import { promisify } from "node:util";
|
|
|
1167
1282
|
var run = promisify(execFile);
|
|
1168
1283
|
async function readGitOrigin(dir) {
|
|
1169
1284
|
try {
|
|
1170
|
-
const root = await git(dir, "rev-parse", "--show-toplevel");
|
|
1171
|
-
const url = await git(dir, "config", "--get", "remote.origin.url");
|
|
1285
|
+
const [root, url] = await Promise.all([git(dir, "rev-parse", "--show-toplevel"), git(dir, "config", "--get", "remote.origin.url")]);
|
|
1172
1286
|
return { root, url };
|
|
1173
1287
|
} catch {
|
|
1174
1288
|
return null;
|
|
@@ -1219,30 +1333,33 @@ async function requireTarget(cwd = process.cwd(), env = process.env) {
|
|
|
1219
1333
|
}
|
|
1220
1334
|
|
|
1221
1335
|
// src/context.ts
|
|
1222
|
-
function requireDeployment() {
|
|
1223
|
-
const
|
|
1336
|
+
async function requireDeployment(repo) {
|
|
1337
|
+
const found = repo ?? (await findTarget(process.cwd(), process.env).catch(() => null))?.repo ?? null;
|
|
1338
|
+
const deployment = await resolveDeployment(found);
|
|
1224
1339
|
if (!deployment) {
|
|
1225
|
-
throw new Error("
|
|
1340
|
+
throw new Error("no noenv account is set up, run `noenv account add NAME URL` or set NOENV_URL");
|
|
1226
1341
|
}
|
|
1227
1342
|
return deployment;
|
|
1228
1343
|
}
|
|
1229
|
-
async function signedIn() {
|
|
1230
|
-
const deployment = requireDeployment();
|
|
1344
|
+
async function signedIn(repo) {
|
|
1345
|
+
const deployment = await requireDeployment(repo);
|
|
1231
1346
|
const token = await requireSession(deployment);
|
|
1232
1347
|
return { deployment, token, identity: await ensureIdentity(deployment, token) };
|
|
1233
1348
|
}
|
|
1234
1349
|
async function inProject() {
|
|
1235
|
-
|
|
1350
|
+
const target = await requireTarget();
|
|
1351
|
+
return { ...await signedIn(target.repo), target };
|
|
1236
1352
|
}
|
|
1237
1353
|
|
|
1238
1354
|
// src/commands/auth.ts
|
|
1239
1355
|
async function authCommand(args2) {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const pending2 = await readPendingSignIn(
|
|
1243
|
-
if (pending2) await waitForSignIn(
|
|
1356
|
+
if (args2[0] === "--wait") {
|
|
1357
|
+
const deployment2 = args2[1] ? deploymentAt(args2[1]) : await requireDeployment();
|
|
1358
|
+
const pending2 = await readPendingSignIn(deployment2);
|
|
1359
|
+
if (pending2) await waitForSignIn(deployment2, pending2);
|
|
1244
1360
|
return;
|
|
1245
1361
|
}
|
|
1362
|
+
const deployment = await accountOrCurrent(args2[0]);
|
|
1246
1363
|
const { pending, started } = await startSignIn(deployment);
|
|
1247
1364
|
console.log(`Open ${pending.url} and confirm the code ${pending.userCode}`);
|
|
1248
1365
|
if (started) openBrowser(pending.url);
|
|
@@ -1250,10 +1367,12 @@ async function authCommand(args2) {
|
|
|
1250
1367
|
throw new Error("sign-in was not completed");
|
|
1251
1368
|
}
|
|
1252
1369
|
const me = await api(deployment, "GET", "/v1/me", { token: await loadSession(deployment) });
|
|
1253
|
-
console.log(`Signed in as ${me.email}`);
|
|
1370
|
+
console.log(`Signed in to ${label(deployment)} as ${me.email}`);
|
|
1254
1371
|
}
|
|
1255
1372
|
async function statusCommand() {
|
|
1256
|
-
const
|
|
1373
|
+
const target = await findTarget(process.cwd(), process.env);
|
|
1374
|
+
const deployment = await requireDeployment(target?.repo);
|
|
1375
|
+
if (deployment.account) console.log(`Account: ${deployment.account}`);
|
|
1257
1376
|
console.log(`Deployment: ${deployment.url}`);
|
|
1258
1377
|
const token = await loadSession(deployment);
|
|
1259
1378
|
if (!token) {
|
|
@@ -1262,7 +1381,6 @@ async function statusCommand() {
|
|
|
1262
1381
|
}
|
|
1263
1382
|
const me = await api(deployment, "GET", "/v1/me", { token });
|
|
1264
1383
|
console.log(`Signed in: ${me.email}${me.isAdmin ? " (organization admin)" : ""}`);
|
|
1265
|
-
const target = await findTarget(process.cwd(), process.env);
|
|
1266
1384
|
if (!target) {
|
|
1267
1385
|
console.log("Project: none (not in a git repository with an origin)");
|
|
1268
1386
|
return;
|
|
@@ -1277,6 +1395,44 @@ async function statusCommand() {
|
|
|
1277
1395
|
console.log("Secrets: no access");
|
|
1278
1396
|
}
|
|
1279
1397
|
}
|
|
1398
|
+
async function accountOrCurrent(name2) {
|
|
1399
|
+
if (!name2) return requireDeployment();
|
|
1400
|
+
return await resolveDeployment(null, { ...process.env, NOENV_ACCOUNT: name2 });
|
|
1401
|
+
}
|
|
1402
|
+
function label(deployment) {
|
|
1403
|
+
return deployment.account ?? deployment.url;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/commands/exec.ts
|
|
1407
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1408
|
+
import { constants } from "node:os";
|
|
1409
|
+
|
|
1410
|
+
// src/bundle.ts
|
|
1411
|
+
async function fetchRawBundle(deployment, token, repo, environment) {
|
|
1412
|
+
return api(deployment, "GET", `${environmentPath(repo, environment)}/bundle`, { token });
|
|
1413
|
+
}
|
|
1414
|
+
async function openBundle(raw, privateKey, environment) {
|
|
1415
|
+
const environmentPrivateKey = await unwrapPrivateKey(raw.wrappedKey, privateKey);
|
|
1416
|
+
const secrets = {};
|
|
1417
|
+
for (const { name: name2, sealed } of raw.secrets) {
|
|
1418
|
+
secrets[name2] = await decryptSecret(environmentPrivateKey, sealed, { projectId: raw.projectId, environment, name: name2 });
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
projectId: raw.projectId,
|
|
1422
|
+
revision: raw.revision,
|
|
1423
|
+
policies: raw.policies,
|
|
1424
|
+
environmentPublicKey: await importPublicKey(raw.publicKey),
|
|
1425
|
+
environmentPrivateKey,
|
|
1426
|
+
secrets
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
async function fetchBundle(deployment, token, privateKey, repo, environment) {
|
|
1430
|
+
return openBundle(await fetchRawBundle(deployment, token, repo, environment), privateKey, environment);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/refresh.ts
|
|
1434
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1435
|
+
import { join as join7 } from "node:path";
|
|
1280
1436
|
|
|
1281
1437
|
// ../packages/policy/src/defaults.ts
|
|
1282
1438
|
var DEFAULT_POLICIES = {
|
|
@@ -1397,27 +1553,6 @@ async function deleteAfterMigration(path, store) {
|
|
|
1397
1553
|
}
|
|
1398
1554
|
}
|
|
1399
1555
|
|
|
1400
|
-
// src/commands/migrate.ts
|
|
1401
|
-
import { relative as relative2 } from "node:path";
|
|
1402
|
-
|
|
1403
|
-
// src/bundle.ts
|
|
1404
|
-
async function fetchBundle(deployment, token, privateKey, repo, environment) {
|
|
1405
|
-
const raw = await api(deployment, "GET", `${environmentPath(repo, environment)}/bundle`, { token });
|
|
1406
|
-
const environmentPrivateKey = await unwrapPrivateKey(raw.wrappedKey, privateKey);
|
|
1407
|
-
const secrets = {};
|
|
1408
|
-
for (const { name: name2, sealed } of raw.secrets) {
|
|
1409
|
-
secrets[name2] = await decryptSecret(environmentPrivateKey, sealed, { projectId: raw.projectId, environment, name: name2 });
|
|
1410
|
-
}
|
|
1411
|
-
return {
|
|
1412
|
-
projectId: raw.projectId,
|
|
1413
|
-
revision: raw.revision,
|
|
1414
|
-
policies: raw.policies,
|
|
1415
|
-
environmentPublicKey: await importPublicKey(raw.publicKey),
|
|
1416
|
-
environmentPrivateKey,
|
|
1417
|
-
secrets
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
1556
|
// src/compliance.ts
|
|
1422
1557
|
import { relative } from "node:path";
|
|
1423
1558
|
var DESCRIPTIONS = {
|
|
@@ -1443,48 +1578,6 @@ async function reportCompliance(deployment, token, target, result) {
|
|
|
1443
1578
|
});
|
|
1444
1579
|
}
|
|
1445
1580
|
|
|
1446
|
-
// src/prompt.ts
|
|
1447
|
-
import { createInterface } from "node:readline/promises";
|
|
1448
|
-
import { text } from "node:stream/consumers";
|
|
1449
|
-
async function readSecretValue(label) {
|
|
1450
|
-
if (!process.stdin.isTTY) {
|
|
1451
|
-
return (await text(process.stdin)).replace(/\r?\n$/, "");
|
|
1452
|
-
}
|
|
1453
|
-
process.stderr.write(label);
|
|
1454
|
-
process.stdin.setRawMode(true);
|
|
1455
|
-
process.stdin.resume();
|
|
1456
|
-
return new Promise((resolve) => {
|
|
1457
|
-
let value = "";
|
|
1458
|
-
const onData = (chunk) => {
|
|
1459
|
-
for (const char of chunk.toString("utf8")) {
|
|
1460
|
-
if (char === "\r" || char === "\n") {
|
|
1461
|
-
finish();
|
|
1462
|
-
return resolve(value);
|
|
1463
|
-
}
|
|
1464
|
-
if (char === "") {
|
|
1465
|
-
finish();
|
|
1466
|
-
process.exit(130);
|
|
1467
|
-
}
|
|
1468
|
-
value = char === "\x7F" ? value.slice(0, -1) : value + char;
|
|
1469
|
-
}
|
|
1470
|
-
};
|
|
1471
|
-
const finish = () => {
|
|
1472
|
-
process.stdin.off("data", onData);
|
|
1473
|
-
process.stdin.setRawMode(false);
|
|
1474
|
-
process.stdin.pause();
|
|
1475
|
-
process.stderr.write("\n");
|
|
1476
|
-
};
|
|
1477
|
-
process.stdin.on("data", onData);
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
async function confirm(question) {
|
|
1481
|
-
if (!process.stdin.isTTY) return false;
|
|
1482
|
-
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
1483
|
-
const answer = await readline.question(`${question} [y/N] `);
|
|
1484
|
-
readline.close();
|
|
1485
|
-
return answer.trim().toLowerCase() === "y";
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1581
|
// src/secret-store.ts
|
|
1489
1582
|
function remoteSecretStore(deployment, token, identity, repo, environment, initial = null) {
|
|
1490
1583
|
let cached = initial;
|
|
@@ -1533,7 +1626,7 @@ async function watchEnvironment(deployment, repo, environment) {
|
|
|
1533
1626
|
}
|
|
1534
1627
|
async function startWatcher(deployment) {
|
|
1535
1628
|
if (await isWatcherRunning(deployment)) return;
|
|
1536
|
-
spawn2(process.execPath, [...process.execArgv, process.argv[1], "watch"], { detached: true, stdio: "ignore" }).unref();
|
|
1629
|
+
spawn2(process.execPath, [...process.execArgv, process.argv[1], "watch", deployment.url], { detached: true, stdio: "ignore" }).unref();
|
|
1537
1630
|
}
|
|
1538
1631
|
async function runWatcher(deployment) {
|
|
1539
1632
|
try {
|
|
@@ -1544,7 +1637,7 @@ async function runWatcher(deployment) {
|
|
|
1544
1637
|
}
|
|
1545
1638
|
try {
|
|
1546
1639
|
while (await checkForUpdates(deployment)) {
|
|
1547
|
-
await new Promise((
|
|
1640
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
1548
1641
|
}
|
|
1549
1642
|
} finally {
|
|
1550
1643
|
await removeFile(pidPath(deployment));
|
|
@@ -1566,7 +1659,8 @@ async function refreshMarkers(deployment, token, entries) {
|
|
|
1566
1659
|
});
|
|
1567
1660
|
for (const { repo, environment, revision } of revisions) {
|
|
1568
1661
|
const path = markerPath(deployment, repo, environment);
|
|
1569
|
-
|
|
1662
|
+
const marker = await readMarker(path);
|
|
1663
|
+
if (marker !== CHANGED && marker.split(":")[0] !== String(revision)) await writeMarker(path, CHANGED);
|
|
1570
1664
|
}
|
|
1571
1665
|
}
|
|
1572
1666
|
async function isWatcherRunning(deployment) {
|
|
@@ -1586,7 +1680,480 @@ function pidPath(deployment) {
|
|
|
1586
1680
|
return join6(deployment.dir, "watcher.pid");
|
|
1587
1681
|
}
|
|
1588
1682
|
|
|
1683
|
+
// src/refresh.ts
|
|
1684
|
+
async function readCache(deployment, target) {
|
|
1685
|
+
return readJsonFile(cachePath(deployment, target)).catch(() => null);
|
|
1686
|
+
}
|
|
1687
|
+
async function refresh(deployment, target) {
|
|
1688
|
+
const entry = await fetchEntry(deployment, target);
|
|
1689
|
+
if (!entry) return null;
|
|
1690
|
+
await writeJsonFile(cachePath(deployment, target), entry);
|
|
1691
|
+
await writeMarker(markerPath(deployment, target.repo, target.environment), `${"raw" in entry ? entry.raw.revision : "x"}:${Date.now()}`);
|
|
1692
|
+
return entry;
|
|
1693
|
+
}
|
|
1694
|
+
async function fetchEntry(deployment, target) {
|
|
1695
|
+
try {
|
|
1696
|
+
const token = await loadSession(deployment);
|
|
1697
|
+
if (!token) return { error: "you are not signed in", status: 401 };
|
|
1698
|
+
const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
|
|
1699
|
+
let raw = await fetchRawBundle(deployment, token, target.repo, target.environment);
|
|
1700
|
+
const bundle = await openBundle(raw, identity.privateKey, target.environment);
|
|
1701
|
+
const policy = bundle.policies.dotenv;
|
|
1702
|
+
const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment, bundle);
|
|
1703
|
+
const result = await enforceDotenvPolicy(target.root, policy, store);
|
|
1704
|
+
const notes = result.files.map((file) => describeOutcome(target, file));
|
|
1705
|
+
if (result.files.length > 0) {
|
|
1706
|
+
await reportCompliance(deployment, token, target, result).catch(() => notes.push("could not report compliance"));
|
|
1707
|
+
}
|
|
1708
|
+
if (result.blocked) {
|
|
1709
|
+
notes.push(`secrets were not loaded: the "${policy.mode}" policy does not allow .env files, run \`noenv migrate\``);
|
|
1710
|
+
}
|
|
1711
|
+
if (result.files.some((file) => file.status === "deleted")) {
|
|
1712
|
+
raw = await fetchRawBundle(deployment, token, target.repo, target.environment);
|
|
1713
|
+
}
|
|
1714
|
+
return { raw, notes, blocked: result.blocked };
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
if (!(error instanceof ApiError)) return null;
|
|
1717
|
+
if (error.status === 401) await clearSession(deployment);
|
|
1718
|
+
return { error: error.message, status: error.status };
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function cachePath(deployment, target) {
|
|
1722
|
+
const name2 = createHash2("sha256").update(`${target.repo}\0${target.environment}`).digest("hex").slice(0, 32);
|
|
1723
|
+
return join7(deployment.dir, "bundles", `${name2}.json`);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/shell.ts
|
|
1727
|
+
var FIND_ROOT = `_noenv_root() {
|
|
1728
|
+
local dir="$PWD"
|
|
1729
|
+
until [[ -e "$dir/.git" || "$dir" == "/" ]]; do dir="\${dir%/*}"; dir="\${dir:-/}"; done
|
|
1730
|
+
if [[ -e "$dir/.git" ]]; then REPLY="$dir"; else REPLY=""; fi
|
|
1731
|
+
}
|
|
1732
|
+
`;
|
|
1733
|
+
var HOOKS = {
|
|
1734
|
+
zsh: `${FIND_ROOT}_noenv_hook() {
|
|
1735
|
+
local revision="" REPLY
|
|
1736
|
+
_noenv_root
|
|
1737
|
+
[[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
|
|
1738
|
+
if [[ "$REPLY" != "$_NOENV_ROOT" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
|
|
1739
|
+
_NOENV_ROOT="$REPLY"
|
|
1740
|
+
if [[ -z "$REPLY" && ( -n "$_NOENV_RESTORE" || -z "$_NOENV_STATE" ) ]]; then
|
|
1741
|
+
eval "$_NOENV_RESTORE"
|
|
1742
|
+
else
|
|
1743
|
+
eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env zsh)"
|
|
1744
|
+
fi
|
|
1745
|
+
fi
|
|
1746
|
+
}
|
|
1747
|
+
_noenv_refresh() {
|
|
1748
|
+
local fd
|
|
1749
|
+
exec {fd}< <(command noenv refresh "$@" 2>/dev/null; print)
|
|
1750
|
+
zle -F -w "$fd" _noenv_ready 2>/dev/null || exec {fd}<&-
|
|
1751
|
+
}
|
|
1752
|
+
_noenv_ready() {
|
|
1753
|
+
local fd="$1"
|
|
1754
|
+
zle -F "$fd"
|
|
1755
|
+
exec {fd}<&-
|
|
1756
|
+
zle -I
|
|
1757
|
+
_noenv_hook
|
|
1758
|
+
zle reset-prompt
|
|
1759
|
+
}
|
|
1760
|
+
zle -N _noenv_ready 2>/dev/null
|
|
1761
|
+
typeset -ag precmd_functions chpwd_functions
|
|
1762
|
+
if (( ! \${precmd_functions[(I)_noenv_hook]} )); then precmd_functions=(_noenv_hook $precmd_functions); fi
|
|
1763
|
+
if (( ! \${chpwd_functions[(I)_noenv_hook]} )); then chpwd_functions=(_noenv_hook $chpwd_functions); fi
|
|
1764
|
+
`,
|
|
1765
|
+
bash: `${FIND_ROOT}_noenv_hook() {
|
|
1766
|
+
local previous_exit=$? revision="" REPLY
|
|
1767
|
+
_noenv_root
|
|
1768
|
+
[[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
|
|
1769
|
+
if [[ "$REPLY" != "$_NOENV_ROOT" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
|
|
1770
|
+
_NOENV_ROOT="$REPLY"
|
|
1771
|
+
if [[ -z "$REPLY" && ( -n "$_NOENV_RESTORE" || -z "$_NOENV_STATE" ) ]]; then
|
|
1772
|
+
eval "$_NOENV_RESTORE"
|
|
1773
|
+
else
|
|
1774
|
+
eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env bash)"
|
|
1775
|
+
fi
|
|
1776
|
+
fi
|
|
1777
|
+
return $previous_exit
|
|
1778
|
+
}
|
|
1779
|
+
_noenv_refresh() {
|
|
1780
|
+
(command noenv refresh "$@" >/dev/null 2>&1 &)
|
|
1781
|
+
}
|
|
1782
|
+
if [[ ";\${PROMPT_COMMAND:-};" != *";_noenv_hook;"* ]]; then
|
|
1783
|
+
PROMPT_COMMAND="_noenv_hook\${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
|
|
1784
|
+
fi
|
|
1785
|
+
`
|
|
1786
|
+
};
|
|
1787
|
+
function parseShell(value) {
|
|
1788
|
+
if (value !== "zsh" && value !== "bash") {
|
|
1789
|
+
throw new Error("shell must be zsh or bash");
|
|
1790
|
+
}
|
|
1791
|
+
return value;
|
|
1792
|
+
}
|
|
1793
|
+
function hookScript(shell) {
|
|
1794
|
+
return HOOKS[shell];
|
|
1795
|
+
}
|
|
1796
|
+
function quote(value) {
|
|
1797
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1798
|
+
}
|
|
1799
|
+
function exportLine(name2, value) {
|
|
1800
|
+
return `export ${name2}=${quote(value)}`;
|
|
1801
|
+
}
|
|
1802
|
+
function setLine(name2, value) {
|
|
1803
|
+
return `${name2}=${quote(value)}`;
|
|
1804
|
+
}
|
|
1805
|
+
function unsetLine(name2) {
|
|
1806
|
+
return `unset ${name2}`;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/hook-env.ts
|
|
1810
|
+
var STATE_VARIABLES = ["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV", "_NOENV_RESTORE"];
|
|
1811
|
+
async function hookEnv(input) {
|
|
1812
|
+
const state = decodeState(input.state);
|
|
1813
|
+
const messages = [];
|
|
1814
|
+
const resolved = await resolve(input, messages);
|
|
1815
|
+
if (!resolved) {
|
|
1816
|
+
return { lines: state ? [...restoreLines(state), ...STATE_VARIABLES.map(unsetLine)] : [], messages };
|
|
1817
|
+
}
|
|
1818
|
+
const { target, deployment } = resolved;
|
|
1819
|
+
const marker = markerPath(deployment, target.repo, target.environment);
|
|
1820
|
+
const revision = await readMarker(marker);
|
|
1821
|
+
const step = await load(input, resolved, state, revision, messages);
|
|
1822
|
+
return {
|
|
1823
|
+
lines: [
|
|
1824
|
+
...step.lines,
|
|
1825
|
+
exportLine("_NOENV_STATE", encodeState(step.state)),
|
|
1826
|
+
step.state.retry ? setLine("_NOENV_RETRY", "1") : unsetLine("_NOENV_RETRY"),
|
|
1827
|
+
setLine("_NOENV_REV_FILE", marker),
|
|
1828
|
+
setLine("_NOENV_REV", revision),
|
|
1829
|
+
setLine("_NOENV_RESTORE", [...restoreLines(step.state), ...STATE_VARIABLES.map(unsetLine)].join("\n"))
|
|
1830
|
+
],
|
|
1831
|
+
messages,
|
|
1832
|
+
refresh: step.refresh ? { url: deployment.url, root: target.root, environment: target.environment } : void 0
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
async function load(input, resolved, state, revision, messages) {
|
|
1836
|
+
const { target, deployment } = resolved;
|
|
1837
|
+
const entering = state === null || !sameTarget(state, resolved);
|
|
1838
|
+
const empty = { url: deployment.url, repo: target.repo, environment: target.environment, previous: {}, retry: false };
|
|
1839
|
+
const restore = state ? restoreLines(state) : [];
|
|
1840
|
+
if (!await loadSession(deployment)) {
|
|
1841
|
+
return { lines: restore, state: await signIn(input, deployment, empty, state, messages), refresh: false };
|
|
1842
|
+
}
|
|
1843
|
+
const entry = await readCache(deployment, target);
|
|
1844
|
+
const identity = await loadIdentity(deployment);
|
|
1845
|
+
const refresh2 = entering || state.retry || revision === CHANGED || !entry || !identity;
|
|
1846
|
+
if (!entering && !state.retry && (!entry || !identity || "raw" in entry && entry.raw.revision === state.revision)) {
|
|
1847
|
+
return { lines: [], state, refresh: refresh2 };
|
|
1848
|
+
}
|
|
1849
|
+
if (!entry || !identity) {
|
|
1850
|
+
return { lines: restore, state: empty, refresh: refresh2 };
|
|
1851
|
+
}
|
|
1852
|
+
const opened = await openEntry(entry, identity.privateKey, target, messages);
|
|
1853
|
+
if (!opened) {
|
|
1854
|
+
return { lines: restore, state: empty, refresh: refresh2 };
|
|
1855
|
+
}
|
|
1856
|
+
const lines = [...restore];
|
|
1857
|
+
const previous = {};
|
|
1858
|
+
for (const [name2, value] of Object.entries(opened.secrets)) {
|
|
1859
|
+
previous[name2] = state && name2 in state.previous ? state.previous[name2] : input.env[name2] ?? null;
|
|
1860
|
+
lines.push(exportLine(name2, value));
|
|
1861
|
+
}
|
|
1862
|
+
const count = Object.keys(opened.secrets).length;
|
|
1863
|
+
const updated = !entering && state.revision !== void 0;
|
|
1864
|
+
messages.push(`${updated ? "updated" : "loaded"} ${count} secrets for ${target.repo} (${target.environment})`);
|
|
1865
|
+
const missing = target.requiredSecrets.filter((name2) => !(name2 in opened.secrets));
|
|
1866
|
+
if (missing.length > 0) messages.push(`missing required secrets: ${missing.join(", ")}`);
|
|
1867
|
+
await watchEnvironment(deployment, target.repo, target.environment);
|
|
1868
|
+
input.startWatcher(deployment);
|
|
1869
|
+
return { lines, state: { ...empty, previous, revision: opened.revision }, refresh: refresh2 };
|
|
1870
|
+
}
|
|
1871
|
+
async function openEntry(entry, privateKey, target, messages) {
|
|
1872
|
+
if ("error" in entry) {
|
|
1873
|
+
if (!(entry.status === 403 && !target.hasConfig) && entry.status !== 401) {
|
|
1874
|
+
messages.push(`could not load secrets for ${target.repo} (${target.environment}): ${entry.error}`);
|
|
1875
|
+
}
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
messages.push(...entry.notes);
|
|
1879
|
+
if (entry.blocked) return null;
|
|
1880
|
+
try {
|
|
1881
|
+
const bundle = await openBundle(entry.raw, privateKey, target.environment);
|
|
1882
|
+
return { secrets: bundle.secrets, revision: bundle.revision };
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
messages.push(`could not decrypt secrets for ${target.repo} (${target.environment}): ${describe(error)}`);
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function sameTarget(state, { target, deployment }) {
|
|
1889
|
+
return state.url === deployment.url && state.repo === target.repo && state.environment === target.environment;
|
|
1890
|
+
}
|
|
1891
|
+
async function signIn(input, deployment, empty, state, messages) {
|
|
1892
|
+
const waiting = await readPendingSignIn(deployment);
|
|
1893
|
+
if (state?.notified && !(waiting && waiting.expiresAt > Date.now())) {
|
|
1894
|
+
messages.push("sign-in was not completed, run `noenv auth` to try again");
|
|
1895
|
+
return empty;
|
|
1896
|
+
}
|
|
1897
|
+
try {
|
|
1898
|
+
const { pending, started } = await startSignIn(deployment);
|
|
1899
|
+
if (started) {
|
|
1900
|
+
input.startPoller(deployment);
|
|
1901
|
+
input.openBrowser(pending.url);
|
|
1902
|
+
}
|
|
1903
|
+
if (started || state?.notified !== pending.userCode) {
|
|
1904
|
+
const account = deployment.account ? ` to ${deployment.account}` : "";
|
|
1905
|
+
messages.push(`sign in${account} to load secrets: open ${pending.url} and confirm the code ${pending.userCode}`);
|
|
1906
|
+
}
|
|
1907
|
+
return { ...empty, retry: true, notified: pending.userCode };
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
messages.push(`could not start sign-in: ${describe(error)}`);
|
|
1910
|
+
return empty;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
async function resolve(input, messages) {
|
|
1914
|
+
try {
|
|
1915
|
+
const target = await findTarget(input.cwd, input.env);
|
|
1916
|
+
const deployment = target && await input.resolveDeployment(target.repo);
|
|
1917
|
+
return target && deployment ? { target, deployment } : null;
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
messages.push(describe(error));
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function restoreLines(state) {
|
|
1924
|
+
return Object.entries(state.previous).map(([name2, value]) => value === null ? unsetLine(name2) : exportLine(name2, value));
|
|
1925
|
+
}
|
|
1926
|
+
function encodeState(state) {
|
|
1927
|
+
return Buffer.from(JSON.stringify(state)).toString("base64");
|
|
1928
|
+
}
|
|
1929
|
+
function decodeState(text3) {
|
|
1930
|
+
if (!text3?.trim()) return null;
|
|
1931
|
+
try {
|
|
1932
|
+
return JSON.parse(Buffer.from(text3.trim(), "base64").toString("utf8"));
|
|
1933
|
+
} catch {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
function describe(error) {
|
|
1938
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/commands/exec.ts
|
|
1942
|
+
var SHELL_STATE = ["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV", "_NOENV_ROOT"];
|
|
1943
|
+
async function execCommand(args2) {
|
|
1944
|
+
const command2 = args2[0] === "--" ? args2.slice(1) : args2;
|
|
1945
|
+
if (command2.length === 0) throw new Error("usage: noenv exec -- COMMAND [ARGS...]");
|
|
1946
|
+
const env = withoutLoadedSecrets(process.env);
|
|
1947
|
+
const secrets = await projectSecrets(env);
|
|
1948
|
+
const child = spawn3(command2[0], command2.slice(1), { stdio: "inherit", env: { ...env, ...secrets } });
|
|
1949
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => child.kill(signal));
|
|
1950
|
+
const code = await new Promise((resolve2, reject) => {
|
|
1951
|
+
child.on("error", reject);
|
|
1952
|
+
child.on("exit", (exitCode, signal) => resolve2(exitCode ?? 128 + (signal ? constants.signals[signal] : 0)));
|
|
1953
|
+
});
|
|
1954
|
+
process.exit(code);
|
|
1955
|
+
}
|
|
1956
|
+
function withoutLoadedSecrets(env) {
|
|
1957
|
+
const state = decodeState(env._NOENV_STATE);
|
|
1958
|
+
const clean = { ...env };
|
|
1959
|
+
for (const name2 of SHELL_STATE) delete clean[name2];
|
|
1960
|
+
for (const [name2, value] of Object.entries(state?.previous ?? {})) {
|
|
1961
|
+
if (value === null) delete clean[name2];
|
|
1962
|
+
else clean[name2] = value;
|
|
1963
|
+
}
|
|
1964
|
+
return clean;
|
|
1965
|
+
}
|
|
1966
|
+
async function projectSecrets(env) {
|
|
1967
|
+
const target = await findTarget(process.cwd(), env);
|
|
1968
|
+
if (!target) return note("not in a git repository with an origin, running without secrets");
|
|
1969
|
+
const deployment = await resolveDeployment(target.repo, env);
|
|
1970
|
+
if (!deployment) return note("no account is set up, running without secrets");
|
|
1971
|
+
const token = await requireSession(deployment);
|
|
1972
|
+
const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
|
|
1973
|
+
const label2 = `${target.repo} (${target.environment})`;
|
|
1974
|
+
let entry = await refresh(deployment, target);
|
|
1975
|
+
if (!entry) {
|
|
1976
|
+
entry = await readCache(deployment, target);
|
|
1977
|
+
if (!entry) throw new Error(`could not reach ${deployment.url} and there are no cached secrets for ${label2}`);
|
|
1978
|
+
note(`could not reach ${deployment.url}, using cached secrets`);
|
|
1979
|
+
}
|
|
1980
|
+
if ("error" in entry) {
|
|
1981
|
+
if ((entry.status === 403 || entry.status === 404) && !target.hasConfig) {
|
|
1982
|
+
return note(`you have no noenv project for ${target.repo} on ${deployment.account ?? deployment.url}, running without secrets`);
|
|
1983
|
+
}
|
|
1984
|
+
throw new Error(`could not load secrets for ${label2}: ${entry.error}`);
|
|
1985
|
+
}
|
|
1986
|
+
for (const line of entry.notes) note(line);
|
|
1987
|
+
if (entry.blocked) throw new Error(`secrets for ${label2} were not loaded because of the .env policy`);
|
|
1988
|
+
const { secrets } = await openBundle(entry.raw, identity.privateKey, target.environment);
|
|
1989
|
+
note(`loaded ${Object.keys(secrets).length} secrets for ${label2}${deployment.account ? ` from ${deployment.account}` : ""}`);
|
|
1990
|
+
const missing = target.requiredSecrets.filter((name2) => !(name2 in secrets));
|
|
1991
|
+
if (missing.length > 0) note(`missing required secrets: ${missing.join(", ")}`);
|
|
1992
|
+
return secrets;
|
|
1993
|
+
}
|
|
1994
|
+
function note(message) {
|
|
1995
|
+
process.stderr.write(`noenv: ${message}
|
|
1996
|
+
`);
|
|
1997
|
+
return {};
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/prompt.ts
|
|
2001
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2002
|
+
import { createInterface } from "node:readline/promises";
|
|
2003
|
+
import { text } from "node:stream/consumers";
|
|
2004
|
+
async function readSecretValue(label2) {
|
|
2005
|
+
if (!process.stdin.isTTY) {
|
|
2006
|
+
return (await text(process.stdin)).replace(/\r?\n$/, "");
|
|
2007
|
+
}
|
|
2008
|
+
process.stderr.write(label2);
|
|
2009
|
+
process.stdin.setRawMode(true);
|
|
2010
|
+
process.stdin.resume();
|
|
2011
|
+
return new Promise((resolve2) => {
|
|
2012
|
+
let value = "";
|
|
2013
|
+
const onData = (chunk) => {
|
|
2014
|
+
for (const char of chunk.toString("utf8")) {
|
|
2015
|
+
if (char === "\r" || char === "\n") {
|
|
2016
|
+
finish();
|
|
2017
|
+
return resolve2(value);
|
|
2018
|
+
}
|
|
2019
|
+
if (char === "") {
|
|
2020
|
+
finish();
|
|
2021
|
+
process.exit(130);
|
|
2022
|
+
}
|
|
2023
|
+
value = char === "\x7F" ? value.slice(0, -1) : value + char;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
const finish = () => {
|
|
2027
|
+
process.stdin.off("data", onData);
|
|
2028
|
+
process.stdin.setRawMode(false);
|
|
2029
|
+
process.stdin.pause();
|
|
2030
|
+
process.stderr.write("\n");
|
|
2031
|
+
};
|
|
2032
|
+
process.stdin.on("data", onData);
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
async function confirm(question) {
|
|
2036
|
+
if (!process.stdin.isTTY) return false;
|
|
2037
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
2038
|
+
const answer = await readline.question(`${question} [y/N] `);
|
|
2039
|
+
readline.close();
|
|
2040
|
+
return answer.trim().toLowerCase() === "y";
|
|
2041
|
+
}
|
|
2042
|
+
async function checklist(title, items, checked) {
|
|
2043
|
+
const input = process.stdin;
|
|
2044
|
+
const output = process.stderr;
|
|
2045
|
+
const selected = new Set([...checked].filter((item) => items.includes(item)));
|
|
2046
|
+
const height = Math.min(items.length, Math.max(5, (output.rows ?? 24) - 3));
|
|
2047
|
+
let cursor = 0;
|
|
2048
|
+
let top = 0;
|
|
2049
|
+
let drawn = 0;
|
|
2050
|
+
const render = () => {
|
|
2051
|
+
top = Math.min(Math.max(top, cursor - height + 1), cursor);
|
|
2052
|
+
const rows = items.slice(top, top + height).map((item, offset) => {
|
|
2053
|
+
const active = top + offset === cursor;
|
|
2054
|
+
const line = `${active ? "\u203A" : " "} ${selected.has(item) ? "[x]" : "[ ]"} ${item}`;
|
|
2055
|
+
return active ? `\x1B[1m${line}\x1B[22m` : line;
|
|
2056
|
+
});
|
|
2057
|
+
const help = `\x1B[2m${selected.size} of ${items.length} selected \xB7 space select \xB7 a all \xB7 enter confirm \xB7 esc cancel\x1B[22m`;
|
|
2058
|
+
const lines = [title, ...rows, help];
|
|
2059
|
+
output.write((drawn > 0 ? `\x1B[${drawn}A` : "") + lines.map((line) => `\x1B[2K${line}
|
|
2060
|
+
`).join(""));
|
|
2061
|
+
drawn = lines.length;
|
|
2062
|
+
};
|
|
2063
|
+
emitKeypressEvents(input);
|
|
2064
|
+
input.setRawMode(true);
|
|
2065
|
+
input.resume();
|
|
2066
|
+
output.write("\x1B[?25l");
|
|
2067
|
+
return new Promise((resolve2) => {
|
|
2068
|
+
const finish = (result) => {
|
|
2069
|
+
input.off("keypress", onKey);
|
|
2070
|
+
input.setRawMode(false);
|
|
2071
|
+
input.pause();
|
|
2072
|
+
output.write("\x1B[?25h");
|
|
2073
|
+
resolve2(result);
|
|
2074
|
+
};
|
|
2075
|
+
const onKey = (_text, key = {}) => {
|
|
2076
|
+
if (key.ctrl && key.name === "c" || key.name === "escape" || key.name === "q") return finish(null);
|
|
2077
|
+
if (key.name === "return") return finish(items.filter((item) => selected.has(item)));
|
|
2078
|
+
if (key.name === "up" || key.name === "k") cursor = (cursor - 1 + items.length) % items.length;
|
|
2079
|
+
if (key.name === "down" || key.name === "j") cursor = (cursor + 1) % items.length;
|
|
2080
|
+
if (key.name === "space") {
|
|
2081
|
+
const item = items[cursor];
|
|
2082
|
+
if (selected.has(item)) selected.delete(item);
|
|
2083
|
+
else selected.add(item);
|
|
2084
|
+
}
|
|
2085
|
+
if (key.name === "a") {
|
|
2086
|
+
const all = items.every((item) => selected.has(item));
|
|
2087
|
+
for (const item of items) all ? selected.delete(item) : selected.add(item);
|
|
2088
|
+
}
|
|
2089
|
+
render();
|
|
2090
|
+
};
|
|
2091
|
+
input.on("keypress", onKey);
|
|
2092
|
+
render();
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/wrangler.ts
|
|
2097
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
2098
|
+
import { access } from "node:fs/promises";
|
|
2099
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
2100
|
+
function wranglerSource(target) {
|
|
2101
|
+
return target.wrangler.environment ?? target.environment;
|
|
2102
|
+
}
|
|
2103
|
+
async function putWranglerSecrets(target, secrets) {
|
|
2104
|
+
const names = Object.keys(secrets);
|
|
2105
|
+
const env = target.wrangler.env ? ` --env ${target.wrangler.env}` : "";
|
|
2106
|
+
console.log(`Pushing ${names.length} secrets from noenv (${wranglerSource(target)}) to Wrangler${env}: ${names.join(", ")}`);
|
|
2107
|
+
const args2 = ["secret", "bulk", ...target.wrangler.env ? ["--env", target.wrangler.env] : []];
|
|
2108
|
+
const child = spawn4(await findWrangler(target.root), args2, { cwd: target.root, stdio: ["pipe", "inherit", "inherit"] });
|
|
2109
|
+
child.stdin.end(JSON.stringify(secrets));
|
|
2110
|
+
const code = await new Promise((resolve2, reject) => {
|
|
2111
|
+
child.on(
|
|
2112
|
+
"error",
|
|
2113
|
+
(error) => reject(error.code === "ENOENT" ? new Error("wrangler is not installed, run `npm install -D wrangler`") : error)
|
|
2114
|
+
);
|
|
2115
|
+
child.on("exit", (exitCode) => resolve2(exitCode ?? 1));
|
|
2116
|
+
});
|
|
2117
|
+
if (code !== 0) throw new Error(`wrangler secret bulk failed with exit code ${code}`);
|
|
2118
|
+
}
|
|
2119
|
+
async function findWrangler(root) {
|
|
2120
|
+
for (let dir = root; ; dir = dirname2(dir)) {
|
|
2121
|
+
const local = join8(dir, "node_modules", ".bin", "wrangler");
|
|
2122
|
+
if (await access(local).then(() => true, () => false)) return local;
|
|
2123
|
+
if (dirname2(dir) === dir) return "wrangler";
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/commands/wrangler.ts
|
|
2128
|
+
async function wranglerCommand(args2) {
|
|
2129
|
+
const invalid = args2.find((name2) => !ENV_VAR_NAME.test(name2));
|
|
2130
|
+
if (invalid) throw new Error(`usage: noenv wrangler [KEY...] (${invalid} is not a variable name)`);
|
|
2131
|
+
const { deployment, token, identity, target } = await inProject();
|
|
2132
|
+
const environment = wranglerSource(target);
|
|
2133
|
+
const label2 = `${target.repo} (${environment})`;
|
|
2134
|
+
const { secrets } = await fetchBundle(deployment, token, identity.privateKey, target.repo, environment);
|
|
2135
|
+
const names = Object.keys(secrets).sort();
|
|
2136
|
+
if (names.length === 0) throw new Error(`there are no secrets in ${label2}`);
|
|
2137
|
+
const selected = await choose(args2, names, target.wrangler.secrets, label2);
|
|
2138
|
+
if (!selected) return console.log("Cancelled, nothing was pushed");
|
|
2139
|
+
if (selected.length === 0) return console.log("Nothing selected, nothing was pushed");
|
|
2140
|
+
const missing = selected.filter((name2) => !(name2 in secrets));
|
|
2141
|
+
if (missing.length > 0) throw new Error(`${label2} has no secret named ${missing.join(", ")}`);
|
|
2142
|
+
await putWranglerSecrets(target, Object.fromEntries(selected.map((name2) => [name2, secrets[name2]])));
|
|
2143
|
+
}
|
|
2144
|
+
async function choose(args2, names, configured, label2) {
|
|
2145
|
+
if (args2.length > 0) return args2;
|
|
2146
|
+
if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
2147
|
+
return checklist(`Push secrets from ${label2} to Wrangler`, names, configured);
|
|
2148
|
+
}
|
|
2149
|
+
if (configured.length === 0) {
|
|
2150
|
+
throw new Error("pass the KEY names, or list them under [wrangler] secrets in .noenv");
|
|
2151
|
+
}
|
|
2152
|
+
return configured;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1589
2155
|
// src/commands/migrate.ts
|
|
2156
|
+
import { relative as relative2 } from "node:path";
|
|
1590
2157
|
async function migrateCommand() {
|
|
1591
2158
|
const { deployment, token, identity, target } = await inProject();
|
|
1592
2159
|
const bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
@@ -1633,7 +2200,8 @@ async function migrateCommand() {
|
|
|
1633
2200
|
}
|
|
1634
2201
|
|
|
1635
2202
|
// src/commands/project.ts
|
|
1636
|
-
import {
|
|
2203
|
+
import { writeFile as writeFile3 } from "node:fs/promises";
|
|
2204
|
+
import { basename, join as join9 } from "node:path";
|
|
1637
2205
|
var ROLES = ["reader", "editor", "admin"];
|
|
1638
2206
|
async function initCommand() {
|
|
1639
2207
|
const { deployment, token, identity, target } = await inProject();
|
|
@@ -1655,6 +2223,14 @@ async function initCommand() {
|
|
|
1655
2223
|
if (!(error instanceof ApiError && error.status === 409)) throw error;
|
|
1656
2224
|
console.log(`Environment ${target.environment} already exists`);
|
|
1657
2225
|
}
|
|
2226
|
+
try {
|
|
2227
|
+
await writeFile3(join9(target.root, CONFIG_FILE), `version = 1
|
|
2228
|
+
environment = "${target.environment}"
|
|
2229
|
+
`, { flag: "wx" });
|
|
2230
|
+
console.log(`Created ${CONFIG_FILE}, commit it so teammates load this project`);
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
if (error.code !== "EEXIST") throw error;
|
|
2233
|
+
}
|
|
1658
2234
|
}
|
|
1659
2235
|
async function inviteCommand([email]) {
|
|
1660
2236
|
if (!email) throw new Error("usage: noenv invite EMAIL");
|
|
@@ -1719,6 +2295,7 @@ async function setSecret([name2]) {
|
|
|
1719
2295
|
const version = await store.set(name2, value);
|
|
1720
2296
|
await markChanged(deployment, target.repo, target.environment);
|
|
1721
2297
|
console.log(`Saved ${name2} as version ${version} in ${target.repo} (${target.environment})`);
|
|
2298
|
+
await syncToWrangler(target, name2, async () => value);
|
|
1722
2299
|
}
|
|
1723
2300
|
async function listSecrets() {
|
|
1724
2301
|
const { deployment, token, target } = await inProject();
|
|
@@ -1751,7 +2328,7 @@ async function rollbackSecret([name2, version]) {
|
|
|
1751
2328
|
if (!/^\d+$/.test(version ?? "")) {
|
|
1752
2329
|
throw new Error("usage: noenv secret rollback KEY VERSION");
|
|
1753
2330
|
}
|
|
1754
|
-
const { deployment, token, target } = await inProject();
|
|
2331
|
+
const { deployment, token, identity, target } = await inProject();
|
|
1755
2332
|
const result = await api(
|
|
1756
2333
|
deployment,
|
|
1757
2334
|
"POST",
|
|
@@ -1760,250 +2337,103 @@ async function rollbackSecret([name2, version]) {
|
|
|
1760
2337
|
);
|
|
1761
2338
|
await markChanged(deployment, target.repo, target.environment);
|
|
1762
2339
|
console.log(`Restored ${name2} v${version} as v${result.version}`);
|
|
2340
|
+
await syncToWrangler(target, name2, async () => {
|
|
2341
|
+
const { secrets } = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
2342
|
+
return secrets[name2];
|
|
2343
|
+
});
|
|
1763
2344
|
}
|
|
1764
|
-
function
|
|
1765
|
-
if (!
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
// src/commands/shell.ts
|
|
1771
|
-
import { text as text2 } from "node:stream/consumers";
|
|
1772
|
-
|
|
1773
|
-
// src/shell.ts
|
|
1774
|
-
var HOOKS = {
|
|
1775
|
-
zsh: `_noenv_hook() {
|
|
1776
|
-
local revision=""
|
|
1777
|
-
[[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
|
|
1778
|
-
if [[ "$PWD" != "$_NOENV_PWD" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
|
|
1779
|
-
_NOENV_PWD="$PWD"
|
|
1780
|
-
eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env zsh)"
|
|
1781
|
-
fi
|
|
1782
|
-
}
|
|
1783
|
-
typeset -ag precmd_functions chpwd_functions
|
|
1784
|
-
if (( ! \${precmd_functions[(I)_noenv_hook]} )); then precmd_functions=(_noenv_hook $precmd_functions); fi
|
|
1785
|
-
if (( ! \${chpwd_functions[(I)_noenv_hook]} )); then chpwd_functions=(_noenv_hook $chpwd_functions); fi
|
|
1786
|
-
`,
|
|
1787
|
-
bash: `_noenv_hook() {
|
|
1788
|
-
local previous_exit=$? revision=""
|
|
1789
|
-
[[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
|
|
1790
|
-
if [[ "$PWD" != "$_NOENV_PWD" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
|
|
1791
|
-
_NOENV_PWD="$PWD"
|
|
1792
|
-
eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env bash)"
|
|
1793
|
-
fi
|
|
1794
|
-
return $previous_exit
|
|
1795
|
-
}
|
|
1796
|
-
if [[ ";\${PROMPT_COMMAND:-};" != *";_noenv_hook;"* ]]; then
|
|
1797
|
-
PROMPT_COMMAND="_noenv_hook\${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
|
|
1798
|
-
fi
|
|
1799
|
-
`
|
|
1800
|
-
};
|
|
1801
|
-
function parseShell(value) {
|
|
1802
|
-
if (value !== "zsh" && value !== "bash") {
|
|
1803
|
-
throw new Error("shell must be zsh or bash");
|
|
1804
|
-
}
|
|
1805
|
-
return value;
|
|
1806
|
-
}
|
|
1807
|
-
function hookScript(shell) {
|
|
1808
|
-
return HOOKS[shell];
|
|
1809
|
-
}
|
|
1810
|
-
function quote(value) {
|
|
1811
|
-
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1812
|
-
}
|
|
1813
|
-
function exportLine(name2, value) {
|
|
1814
|
-
return `export ${name2}=${quote(value)}`;
|
|
1815
|
-
}
|
|
1816
|
-
function setLine(name2, value) {
|
|
1817
|
-
return `${name2}=${quote(value)}`;
|
|
1818
|
-
}
|
|
1819
|
-
function unsetLine(name2) {
|
|
1820
|
-
return `unset ${name2}`;
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
// src/hook-env.ts
|
|
1824
|
-
async function hookEnv(input) {
|
|
1825
|
-
const state = decodeState(input.state);
|
|
1826
|
-
const messages = [];
|
|
1827
|
-
const target = input.deployment ? await findTargetOrReport(input, messages) : null;
|
|
1828
|
-
if (state && target && !state.retry && sameTarget(state, target) && await isCurrent(input, state)) {
|
|
1829
|
-
return { lines: [], messages };
|
|
1830
|
-
}
|
|
1831
|
-
const lines = state ? restoreLines(state) : [];
|
|
1832
|
-
if (!target) {
|
|
1833
|
-
if (state) lines.push(...["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV"].map(unsetLine));
|
|
1834
|
-
return { lines, messages };
|
|
1835
|
-
}
|
|
1836
|
-
const loaded = await load(input, target, state, messages);
|
|
1837
|
-
lines.push(
|
|
1838
|
-
...loaded.lines,
|
|
1839
|
-
setLine("_NOENV_STATE", encodeState(loaded.state)),
|
|
1840
|
-
loaded.state.retry ? setLine("_NOENV_RETRY", "1") : unsetLine("_NOENV_RETRY"),
|
|
1841
|
-
...revisionLines(input, target, loaded.state.revision)
|
|
1842
|
-
);
|
|
1843
|
-
return { lines, messages };
|
|
1844
|
-
}
|
|
1845
|
-
function sameTarget(state, target) {
|
|
1846
|
-
return state.repo === target.repo && state.environment === target.environment;
|
|
1847
|
-
}
|
|
1848
|
-
async function isCurrent(input, state) {
|
|
1849
|
-
if (state.revision === void 0 || !input.deployment) return true;
|
|
1850
|
-
return await readMarker(markerPath(input.deployment, state.repo, state.environment)) === String(state.revision);
|
|
1851
|
-
}
|
|
1852
|
-
function revisionLines(input, target, revision) {
|
|
1853
|
-
if (revision === void 0 || !input.deployment) return [unsetLine("_NOENV_REV_FILE"), unsetLine("_NOENV_REV")];
|
|
1854
|
-
return [setLine("_NOENV_REV_FILE", markerPath(input.deployment, target.repo, target.environment)), setLine("_NOENV_REV", String(revision))];
|
|
1855
|
-
}
|
|
1856
|
-
async function load(input, target, state, messages) {
|
|
1857
|
-
const { deployment } = input;
|
|
1858
|
-
const empty = { repo: target.repo, environment: target.environment, previous: {}, retry: false };
|
|
1859
|
-
try {
|
|
1860
|
-
const token = await loadSession(deployment);
|
|
1861
|
-
if (!token) {
|
|
1862
|
-
return { lines: [], state: await signIn(input, empty, state, messages) };
|
|
1863
|
-
}
|
|
1864
|
-
const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
|
|
1865
|
-
let bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
1866
|
-
const dotenv = await enforceDotenv({ deployment, token, identity, target, bundle }, messages);
|
|
1867
|
-
if (dotenv.blocked) {
|
|
1868
|
-
return { lines: [], state: empty };
|
|
1869
|
-
}
|
|
1870
|
-
if (dotenv.changed) {
|
|
1871
|
-
bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
1872
|
-
}
|
|
1873
|
-
const lines = [];
|
|
1874
|
-
const previous = {};
|
|
1875
|
-
for (const [name2, value] of Object.entries(bundle.secrets)) {
|
|
1876
|
-
previous[name2] = state && name2 in state.previous ? state.previous[name2] : input.env[name2] ?? null;
|
|
1877
|
-
lines.push(exportLine(name2, value));
|
|
1878
|
-
}
|
|
1879
|
-
const reloaded = state !== null && sameTarget(state, target) && state.revision !== void 0;
|
|
1880
|
-
messages.push(`${reloaded ? "updated" : "loaded"} ${lines.length} secrets for ${target.repo} (${target.environment})`);
|
|
1881
|
-
const missing = target.requiredSecrets.filter((name2) => !(name2 in bundle.secrets));
|
|
1882
|
-
if (missing.length > 0) {
|
|
1883
|
-
messages.push(`missing required secrets: ${missing.join(", ")}`);
|
|
1884
|
-
}
|
|
1885
|
-
await writeMarker(markerPath(deployment, target.repo, target.environment), String(bundle.revision));
|
|
1886
|
-
await watchEnvironment(deployment, target.repo, target.environment);
|
|
1887
|
-
input.startWatcher();
|
|
1888
|
-
return { lines, state: { ...empty, previous, revision: bundle.revision } };
|
|
1889
|
-
} catch (error) {
|
|
1890
|
-
if (error instanceof ApiError && error.status === 401) {
|
|
1891
|
-
await clearSession(deployment);
|
|
1892
|
-
return { lines: [], state: { ...empty, retry: true } };
|
|
1893
|
-
}
|
|
1894
|
-
if (!(error instanceof ApiError && error.status === 403 && !target.hasConfig)) {
|
|
1895
|
-
messages.push(`could not load secrets for ${target.repo} (${target.environment}): ${describe(error)}`);
|
|
1896
|
-
}
|
|
1897
|
-
return { lines: [], state: empty };
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
async function enforceDotenv(loaded, messages) {
|
|
1901
|
-
const { deployment, token, identity, target, bundle } = loaded;
|
|
1902
|
-
const policy = bundle.policies.dotenv;
|
|
1903
|
-
const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment, bundle);
|
|
1904
|
-
const result = await enforceDotenvPolicy(target.root, policy, store);
|
|
1905
|
-
if (result.files.length === 0) {
|
|
1906
|
-
return { blocked: false, changed: false };
|
|
1907
|
-
}
|
|
1908
|
-
messages.push(...result.files.map((file) => describeOutcome(target, file)));
|
|
1909
|
-
if (result.blocked) {
|
|
1910
|
-
messages.push(`secrets were not loaded: the "${policy.mode}" policy does not allow .env files, run \`noenv migrate\``);
|
|
1911
|
-
}
|
|
1912
|
-
await reportCompliance(deployment, token, target, result).catch(() => messages.push("could not report compliance"));
|
|
1913
|
-
return { blocked: result.blocked, changed: result.files.some((file) => file.status === "deleted") };
|
|
1914
|
-
}
|
|
1915
|
-
async function signIn(input, empty, state, messages) {
|
|
1916
|
-
const waiting = await readPendingSignIn(input.deployment);
|
|
1917
|
-
if (state?.notified && !(waiting && waiting.expiresAt > Date.now())) {
|
|
1918
|
-
messages.push("sign-in was not completed, run `noenv auth` to try again");
|
|
1919
|
-
return empty;
|
|
1920
|
-
}
|
|
1921
|
-
const { pending, started } = await startSignIn(input.deployment);
|
|
1922
|
-
if (started) {
|
|
1923
|
-
input.startPoller();
|
|
1924
|
-
input.openBrowser(pending.url);
|
|
1925
|
-
}
|
|
1926
|
-
if (started || state?.notified !== pending.userCode) {
|
|
1927
|
-
messages.push(`sign in to load secrets: open ${pending.url} and confirm the code ${pending.userCode}`);
|
|
2345
|
+
async function syncToWrangler(target, name2, value) {
|
|
2346
|
+
if (!target.wrangler.secrets.includes(name2)) return;
|
|
2347
|
+
if (wranglerSource(target) !== target.environment) {
|
|
2348
|
+
console.log(`Not pushed to Wrangler: .noenv pushes values from ${wranglerSource(target)}, not ${target.environment}`);
|
|
2349
|
+
return;
|
|
1928
2350
|
}
|
|
1929
|
-
return { ...empty, retry: true, notified: pending.userCode };
|
|
1930
|
-
}
|
|
1931
|
-
async function findTargetOrReport(input, messages) {
|
|
1932
2351
|
try {
|
|
1933
|
-
|
|
2352
|
+
await putWranglerSecrets(target, { [name2]: await value() });
|
|
1934
2353
|
} catch (error) {
|
|
1935
|
-
|
|
1936
|
-
|
|
2354
|
+
const reason = error instanceof Error ? error.message : "unknown error";
|
|
2355
|
+
throw new Error(`${name2} is saved in noenv, but the Wrangler update failed: ${reason}. Run \`noenv wrangler ${name2}\` to try again`);
|
|
1937
2356
|
}
|
|
1938
2357
|
}
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
function encodeState(state) {
|
|
1943
|
-
return Buffer.from(JSON.stringify(state)).toString("base64");
|
|
1944
|
-
}
|
|
1945
|
-
function decodeState(text3) {
|
|
1946
|
-
if (!text3.trim()) return null;
|
|
1947
|
-
try {
|
|
1948
|
-
return JSON.parse(Buffer.from(text3.trim(), "base64").toString("utf8"));
|
|
1949
|
-
} catch {
|
|
1950
|
-
return null;
|
|
2358
|
+
function requireName(name2) {
|
|
2359
|
+
if (!name2 || !ENV_VAR_NAME.test(name2)) {
|
|
2360
|
+
throw new Error("the secret name must be a valid environment variable name");
|
|
1951
2361
|
}
|
|
1952
2362
|
}
|
|
1953
|
-
function describe(error) {
|
|
1954
|
-
return error instanceof Error ? error.message : "unknown error";
|
|
1955
|
-
}
|
|
1956
2363
|
|
|
1957
2364
|
// src/commands/shell.ts
|
|
2365
|
+
import { text as text2 } from "node:stream/consumers";
|
|
1958
2366
|
async function shellCommand([shell]) {
|
|
1959
2367
|
process.stdout.write(hookScript(parseShell(shell)));
|
|
1960
2368
|
}
|
|
1961
2369
|
async function hookEnvCommand([shell]) {
|
|
1962
2370
|
parseShell(shell);
|
|
1963
2371
|
const state = process.stdin.isTTY ? "" : await text2(process.stdin);
|
|
1964
|
-
const
|
|
1965
|
-
const { lines, messages } = await hookEnv({
|
|
2372
|
+
const { lines, messages, refresh: job } = await hookEnv({
|
|
1966
2373
|
cwd: process.cwd(),
|
|
1967
2374
|
env: process.env,
|
|
1968
2375
|
state,
|
|
1969
|
-
|
|
2376
|
+
resolveDeployment: (repo) => resolveDeployment(repo),
|
|
1970
2377
|
startPoller: startBackgroundPoller,
|
|
1971
|
-
startWatcher: () => void
|
|
2378
|
+
startWatcher: (deployment) => void startWatcher(deployment),
|
|
1972
2379
|
openBrowser
|
|
1973
2380
|
});
|
|
1974
2381
|
for (const message of messages) process.stderr.write(`noenv: ${message}
|
|
1975
2382
|
`);
|
|
2383
|
+
if (job) lines.push(`_noenv_refresh ${quote(job.url)} ${quote(job.root)} ${quote(job.environment)}`);
|
|
1976
2384
|
process.stdout.write(lines.join("\n"));
|
|
1977
2385
|
}
|
|
1978
|
-
async function
|
|
1979
|
-
|
|
2386
|
+
async function refreshCommand([url, root, environment]) {
|
|
2387
|
+
if (!url || !root || !environment) throw new Error("usage: noenv refresh URL ROOT ENVIRONMENT");
|
|
2388
|
+
const target = await findTarget(root, { ...process.env, NOENV_ENVIRONMENT: environment });
|
|
2389
|
+
if (target) await refresh(deploymentAt(url), target);
|
|
2390
|
+
}
|
|
2391
|
+
async function watchCommand([url]) {
|
|
2392
|
+
if (!url) throw new Error("usage: noenv watch URL");
|
|
2393
|
+
await runWatcher(deploymentAt(url));
|
|
1980
2394
|
}
|
|
1981
2395
|
|
|
1982
2396
|
// src/main.ts
|
|
1983
|
-
var
|
|
2397
|
+
var USAGE2 = `usage: noenv <command>
|
|
1984
2398
|
|
|
1985
2399
|
shell zsh|bash print the shell hook for your shell profile
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2400
|
+
account add NAME URL [PREFIX...]
|
|
2401
|
+
add an account, such as work or personal; repos
|
|
2402
|
+
under a PREFIX (github.com/acme) use it
|
|
2403
|
+
account list list accounts, * marks the default
|
|
2404
|
+
account default NAME use NAME where no prefix matches
|
|
2405
|
+
account remove NAME forget an account
|
|
2406
|
+
auth [NAME] sign in to an account (default: the one for this repo)
|
|
2407
|
+
status show the account, user, and project
|
|
2408
|
+
init create the project, the environment, and the .noenv file
|
|
2409
|
+
exec -- COMMAND run COMMAND with this repository's secrets, for agents,
|
|
2410
|
+
scripts, and CI; it picks the account and project from
|
|
2411
|
+
the current directory
|
|
1989
2412
|
secret set KEY save a secret (the value is read without echo)
|
|
1990
2413
|
secret list list secrets in this environment
|
|
1991
2414
|
secret history KEY list the versions of a secret
|
|
1992
2415
|
secret rollback KEY VER restore an older version of a secret
|
|
1993
2416
|
migrate move .env files of this repository into noenv
|
|
2417
|
+
wrangler [KEY...] push secrets to the Worker with wrangler; without KEY,
|
|
2418
|
+
pick them in a list (keys under [wrangler] secrets in
|
|
2419
|
+
.noenv are checked; without a terminal, push those)
|
|
1994
2420
|
invite EMAIL invite a user to this deployment
|
|
1995
2421
|
grant EMAIL ROLE give a user access to this project and environment
|
|
1996
2422
|
revoke EMAIL remove a user from this project
|
|
1997
2423
|
`;
|
|
1998
2424
|
var commands = {
|
|
2425
|
+
account: accountCommand,
|
|
1999
2426
|
shell: shellCommand,
|
|
2000
2427
|
"hook-env": hookEnvCommand,
|
|
2428
|
+
refresh: refreshCommand,
|
|
2429
|
+
exec: execCommand,
|
|
2001
2430
|
watch: watchCommand,
|
|
2002
2431
|
auth: authCommand,
|
|
2003
2432
|
status: statusCommand,
|
|
2004
2433
|
init: initCommand,
|
|
2005
2434
|
secret: secretCommand,
|
|
2006
2435
|
migrate: migrateCommand,
|
|
2436
|
+
wrangler: wranglerCommand,
|
|
2007
2437
|
invite: inviteCommand,
|
|
2008
2438
|
grant: grantCommand,
|
|
2009
2439
|
revoke: revokeCommand
|
|
@@ -2011,7 +2441,7 @@ var commands = {
|
|
|
2011
2441
|
var [name, ...args] = process.argv.slice(2);
|
|
2012
2442
|
var command = commands[name ?? ""];
|
|
2013
2443
|
if (!command) {
|
|
2014
|
-
process.stdout.write(
|
|
2444
|
+
process.stdout.write(USAGE2);
|
|
2015
2445
|
process.exit(name ? 1 : 0);
|
|
2016
2446
|
}
|
|
2017
2447
|
try {
|