getmyenv 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +45 -141
- package/dist/commands/claim.js +2 -1
- package/dist/commands/import.js +2 -1
- package/dist/commands/start.js +2 -2
- package/dist/lib/bootstrap.js +3 -2
- package/dist/lib/fs.js +2 -2
- package/dist/lib/keychain.js +2 -1
- package/dist/lib/upload.js +3 -2
- package/dist/lib/vault.d.ts +1 -1
- package/dist/lib/vault.js +3 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -34,6 +34,58 @@ Prompts and status lines go to stderr, so `export --stdout > .env` stays clean.
|
|
|
34
34
|
|
|
35
35
|
Commit `.getmyenv/project.json`. `token.json` and `claim.json` stay local and are gitignored.
|
|
36
36
|
|
|
37
|
+
<!-- cli:help -->
|
|
38
|
+
Output of `npx getmyenv --help` (0.9.2):
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
Usage: getmyenv [options] [command]
|
|
42
|
+
|
|
43
|
+
Keep using environment variables. Stop managing them as files.
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
-V, --version output the version number
|
|
47
|
+
--api-url <url> API base URL
|
|
48
|
+
-h, --help display help for command
|
|
49
|
+
|
|
50
|
+
Commands:
|
|
51
|
+
start [options] Set up this folder (same as bare npx getmyenv).
|
|
52
|
+
Status when already linked.
|
|
53
|
+
run [options] [context] Run a command with a context's variables in its
|
|
54
|
+
environment
|
|
55
|
+
status [options] Show project, token, contexts and the run command
|
|
56
|
+
context List, create or pick contexts
|
|
57
|
+
set [options] <pairs...> Set values: set KEY=VALUE, or set KEY to be
|
|
58
|
+
prompted
|
|
59
|
+
unset [options] <names...> Remove values from a context
|
|
60
|
+
ls [options] Variable names by context with status (never
|
|
61
|
+
values)
|
|
62
|
+
import [options] [file] Encrypt a .env file and upload it (.env.<slug>
|
|
63
|
+
goes to that context)
|
|
64
|
+
export [options] Write a context to a plaintext .env file, for
|
|
65
|
+
tools that need one
|
|
66
|
+
guest [options] Create a guest project without an account. It
|
|
67
|
+
works for 30 days. Claim it within 60.
|
|
68
|
+
claim [options] Claim a guest project (reads .getmyenv/claim.json,
|
|
69
|
+
or asks for the claim code)
|
|
70
|
+
backup [options] Write an encrypted backup of open contexts
|
|
71
|
+
restore [options] <file> Restore a backup into this folder's project (open
|
|
72
|
+
contexts with the same slug)
|
|
73
|
+
lock [options] Forget every unlocked context key on this machine
|
|
74
|
+
(OS keychain)
|
|
75
|
+
unlink [options] Revoke this folder's token and remove local
|
|
76
|
+
.getmyenv files
|
|
77
|
+
help [command] display help for command
|
|
78
|
+
|
|
79
|
+
Environment:
|
|
80
|
+
GETMYENV_TOKEN Token for CI and servers (instead of .getmyenv/token.json). A server token holds its context key.
|
|
81
|
+
GETMYENV_API_URL API base URL (https, or http://localhost)
|
|
82
|
+
GETMYENV_CONFIG_DIR Machine identity and cache dir (default ~/.config/getmyenv)
|
|
83
|
+
GETMYENV_SKIP_UPDATE Set to 1 to skip the update notice
|
|
84
|
+
GETMYENV_KEYCHAIN Set to 0 to always ask for the Vault password
|
|
85
|
+
GETMYENV_MACHINE_LABEL Name shown for this machine in access requests
|
|
86
|
+
```
|
|
87
|
+
<!-- /cli:help -->
|
|
88
|
+
|
|
37
89
|
## Vault password
|
|
38
90
|
|
|
39
91
|
Each context has its own key, sealed to your account. Your Vault password opens your account key. For open contexts, after you enter the Vault password in a terminal, the unlocked context keys stay in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) for 8 hours. The password is never stored.
|
|
@@ -65,7 +117,7 @@ Server tokens are read-only. `set` needs the folder token from browser sign-in,
|
|
|
65
117
|
## Open and Read-only
|
|
66
118
|
|
|
67
119
|
- Open contexts: the CLI can read and write. Browser sign-in gives the folder a token in `.getmyenv/token.json`.
|
|
68
|
-
- Read-only contexts: the CLI can read and export, never write.
|
|
120
|
+
- Read-only contexts: the CLI can read and export, never write. Only the owner sets Read-only values, in the dashboard. Use a server token from Project settings, Tokens (one context, IP allowlist) with `GETMYENV_TOKEN`. Each server token has a new machines rule: ask you on Requests (default), trust machines from its allowed IPs on first read (each entry /24 or narrower, IPv6 /64), or block them. Trusted machines read without asking. `status` shows the rule.
|
|
69
121
|
|
|
70
122
|
## Chaining
|
|
71
123
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
1
2
|
/**
|
|
2
3
|
* Split `run` operands around `--`.
|
|
3
4
|
* `run staging -- npm start`: context staging, command npm start.
|
|
@@ -8,4 +9,5 @@ export declare function splitRunArgs(operands: string[], argv: string[], explain
|
|
|
8
9
|
context?: string;
|
|
9
10
|
command: string[];
|
|
10
11
|
};
|
|
12
|
+
export declare function buildProgram(argv: string[]): Command;
|
|
11
13
|
export declare function runCli(argv: string[]): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { CLI_API_OPTION, CLI_DESCRIPTION, cliCommand, cliEnvHelp, } from "@getmyenv/shared";
|
|
2
3
|
import { backupCommand } from "./commands/backup.js";
|
|
3
4
|
import { claimCommand } from "./commands/claim.js";
|
|
4
5
|
import { contextCreateCommand, contextListCommand, contextUseCommand, } from "./commands/context.js";
|
|
@@ -14,11 +15,6 @@ import { startCommand } from "./commands/start.js";
|
|
|
14
15
|
import { statusCommand } from "./commands/status.js";
|
|
15
16
|
import { unlinkCommand } from "./commands/unlink.js";
|
|
16
17
|
import { CLI_VERSION } from "./lib/api.js";
|
|
17
|
-
const CONTEXT_OPTION = [
|
|
18
|
-
"-c, --context <slug>",
|
|
19
|
-
"Context to use (default: the server token's context, then project.json, then development)",
|
|
20
|
-
];
|
|
21
|
-
const API_OPTION = ["--api-url <url>", "API base URL"];
|
|
22
18
|
/**
|
|
23
19
|
* Split `run` operands around `--`.
|
|
24
20
|
* `run staging -- npm start`: context staging, command npm start.
|
|
@@ -36,23 +32,32 @@ export function splitRunArgs(operands, argv, explain) {
|
|
|
36
32
|
return { context: operands[0], command: [] };
|
|
37
33
|
return { command: operands };
|
|
38
34
|
}
|
|
39
|
-
|
|
35
|
+
/** A command, its arguments, flags and examples, all from the shared CLI spec. */
|
|
36
|
+
function define(parent, name, withApi = true) {
|
|
37
|
+
const spec = cliCommand(name);
|
|
38
|
+
const leaf = spec.name.split(" ").at(-1);
|
|
39
|
+
const cmd = parent.command(leaf, spec.isDefault ? { isDefault: true } : {}).description(spec.description);
|
|
40
|
+
for (const a of spec.args)
|
|
41
|
+
cmd.argument(a.name, a.description ?? "");
|
|
42
|
+
for (const o of spec.options)
|
|
43
|
+
cmd.option(o.flags, o.description, o.defaultValue);
|
|
44
|
+
if (withApi)
|
|
45
|
+
cmd.option(CLI_API_OPTION.flags, CLI_API_OPTION.description);
|
|
46
|
+
if (spec.usage)
|
|
47
|
+
cmd.usage(spec.usage);
|
|
48
|
+
if (spec.examples) {
|
|
49
|
+
cmd.addHelpText("after", ["", "Examples:", ...spec.examples.map((e) => ` ${e}`)].join("\n"));
|
|
50
|
+
}
|
|
51
|
+
return cmd;
|
|
52
|
+
}
|
|
53
|
+
export function buildProgram(argv) {
|
|
40
54
|
const program = new Command();
|
|
41
55
|
program
|
|
42
56
|
.name("getmyenv")
|
|
43
|
-
.description(
|
|
57
|
+
.description(CLI_DESCRIPTION)
|
|
44
58
|
.version(CLI_VERSION)
|
|
45
|
-
.option(
|
|
46
|
-
.addHelpText("after",
|
|
47
|
-
"",
|
|
48
|
-
"Environment:",
|
|
49
|
-
" GETMYENV_TOKEN Token for CI and servers (instead of .getmyenv/token.json). A server token holds its context key.",
|
|
50
|
-
" GETMYENV_API_URL API base URL (https, or http://localhost)",
|
|
51
|
-
" GETMYENV_CONFIG_DIR Machine identity and cache dir (default ~/.config/getmyenv)",
|
|
52
|
-
" GETMYENV_SKIP_UPDATE Set to 1 to skip the update notice",
|
|
53
|
-
" GETMYENV_KEYCHAIN Set to 0 to always ask for the Vault password",
|
|
54
|
-
" GETMYENV_MACHINE_LABEL Name shown for this machine in access requests",
|
|
55
|
-
].join("\n"))
|
|
59
|
+
.option(CLI_API_OPTION.flags, CLI_API_OPTION.description)
|
|
60
|
+
.addHelpText("after", cliEnvHelp())
|
|
56
61
|
.helpCommand(true)
|
|
57
62
|
.action((opts) => startCommand(opts));
|
|
58
63
|
// The root owns --api-url wherever it appears, so hand it to the command that runs.
|
|
@@ -62,29 +67,8 @@ export async function runCli(argv) {
|
|
|
62
67
|
actionCommand.setOptionValue("apiUrl", apiUrl);
|
|
63
68
|
}
|
|
64
69
|
});
|
|
65
|
-
program
|
|
66
|
-
|
|
67
|
-
.description("Set up this folder (same as bare npx getmyenv). Status when already linked.")
|
|
68
|
-
.option("--template <id>", "Add a template's variables and ask for their values (id or link)")
|
|
69
|
-
.option(...API_OPTION)
|
|
70
|
-
.action((opts) => startCommand(opts));
|
|
71
|
-
program
|
|
72
|
-
.command("run")
|
|
73
|
-
.description("Run a command with a context's variables in its environment")
|
|
74
|
-
.usage("[context] [options] -- <command...>")
|
|
75
|
-
.addHelpText("after", [
|
|
76
|
-
"",
|
|
77
|
-
"Examples:",
|
|
78
|
-
" npx getmyenv run -- npm run dev",
|
|
79
|
-
" npx getmyenv run staging -- node server.js",
|
|
80
|
-
" GETMYENV_TOKEN=<server token> npx getmyenv run production -- node server.js",
|
|
81
|
-
].join("\n"))
|
|
82
|
-
.argument("[context]", "Context slug (same as -c)")
|
|
83
|
-
.option(...CONTEXT_OPTION)
|
|
84
|
-
.option("--explain", "List each variable and where its value comes from (names only)")
|
|
85
|
-
.option("--keep-existing", "Keep variables already set in the parent environment")
|
|
86
|
-
.option("--allow-missing", "Run even when some variables have no value in this context")
|
|
87
|
-
.option(...API_OPTION)
|
|
70
|
+
define(program, "start").action((opts) => startCommand(opts));
|
|
71
|
+
define(program, "run")
|
|
88
72
|
.allowUnknownOption(true)
|
|
89
73
|
.allowExcessArguments(true)
|
|
90
74
|
.action(async (_ctx, opts, cmd) => {
|
|
@@ -94,106 +78,26 @@ export async function runCli(argv) {
|
|
|
94
78
|
}
|
|
95
79
|
await runCommand({ ...opts, context: opts.context ?? split.context }, split.command);
|
|
96
80
|
});
|
|
97
|
-
program
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.description("Set this folder's default context (saved in project.json)")
|
|
118
|
-
.option(...API_OPTION)
|
|
119
|
-
.action((slug, opts) => contextUseCommand(slug, opts));
|
|
120
|
-
program
|
|
121
|
-
.command("set")
|
|
122
|
-
.description("Set values: set KEY=VALUE, or set KEY to be prompted")
|
|
123
|
-
.argument("<pairs...>", "KEY or KEY=VALUE")
|
|
124
|
-
.option(...CONTEXT_OPTION)
|
|
125
|
-
.option("--expires <date>", "Expiry date, e.g. 2026-12-31, or never")
|
|
126
|
-
.option("--stdin", "Read the value of one KEY from stdin (keeps it out of argv)")
|
|
127
|
-
.option(...API_OPTION)
|
|
128
|
-
.action((pairs, opts) => setCommand(pairs, opts));
|
|
129
|
-
program
|
|
130
|
-
.command("unset")
|
|
131
|
-
.description("Remove values from a context")
|
|
132
|
-
.argument("<names...>", "Variable names")
|
|
133
|
-
.option(...CONTEXT_OPTION)
|
|
134
|
-
.option("--all", "Delete the variable from every context")
|
|
135
|
-
.option(...API_OPTION)
|
|
136
|
-
.action((names, opts) => unsetCommand(names, opts));
|
|
137
|
-
program
|
|
138
|
-
.command("ls")
|
|
139
|
-
.description("Variable names by context with status (never values)")
|
|
140
|
-
.option(...CONTEXT_OPTION)
|
|
141
|
-
.option(...API_OPTION)
|
|
142
|
-
.action((opts) => lsCommand(opts));
|
|
143
|
-
program
|
|
144
|
-
.command("import")
|
|
145
|
-
.description("Encrypt a .env file and upload it (.env.<slug> goes to that context)")
|
|
146
|
-
.argument("[file]", "Env file (default: .env files in this folder)")
|
|
147
|
-
.option(...CONTEXT_OPTION)
|
|
148
|
-
.option("--prune", "Unset values in the context that are not in the file")
|
|
149
|
-
.option("--dry-run", "Show what would change without uploading")
|
|
150
|
-
.option("--yes", "Do not ask for confirmation")
|
|
151
|
-
.option(...API_OPTION)
|
|
152
|
-
.action((file, opts) => importCommand(file, opts));
|
|
153
|
-
program
|
|
154
|
-
.command("export")
|
|
155
|
-
.description("Write a context to a plaintext .env file, for tools that need one")
|
|
156
|
-
.option(...CONTEXT_OPTION)
|
|
157
|
-
.option("-o, --output <file>", "Output file", ".env")
|
|
158
|
-
.option("--stdout", "Write to stdout instead of a file")
|
|
159
|
-
.option("--yes", "Change an existing file without asking")
|
|
160
|
-
.option(...API_OPTION)
|
|
161
|
-
.action((opts) => exportCommand(opts));
|
|
162
|
-
program
|
|
163
|
-
.command("guest")
|
|
164
|
-
.description("Create a guest project without an account. It works for 30 days. Claim it within 60.")
|
|
165
|
-
.option("--name <name>", "Project name (default: folder name)")
|
|
166
|
-
.option(...API_OPTION)
|
|
167
|
-
.action((opts) => guestCommand(opts));
|
|
168
|
-
program
|
|
169
|
-
.command("claim")
|
|
170
|
-
.description("Claim a guest project (reads .getmyenv/claim.json, or asks for the claim code)")
|
|
171
|
-
.option(...API_OPTION)
|
|
172
|
-
.action((opts) => claimCommand(opts));
|
|
173
|
-
program
|
|
174
|
-
.command("backup")
|
|
175
|
-
.description("Write an encrypted backup of open contexts")
|
|
176
|
-
.option("-o, --output <file>", "Output file (default: <project>.getmyenv-backup)")
|
|
177
|
-
.option(...API_OPTION)
|
|
178
|
-
.action((opts) => backupCommand(opts));
|
|
179
|
-
program
|
|
180
|
-
.command("restore <file>")
|
|
181
|
-
.description("Restore a backup into this folder's project (open contexts with the same slug)")
|
|
182
|
-
.option("--dry-run", "Check the backup and show what would be restored")
|
|
183
|
-
.option("--yes", "Do not ask for confirmation")
|
|
184
|
-
.option(...API_OPTION)
|
|
185
|
-
.action((file, opts) => restoreCommand(file, opts));
|
|
186
|
-
program
|
|
187
|
-
.command("lock")
|
|
188
|
-
.description("Forget every unlocked context key on this machine (OS keychain)")
|
|
189
|
-
.option(...API_OPTION)
|
|
190
|
-
.action((opts) => lockCommand(opts));
|
|
191
|
-
program
|
|
192
|
-
.command("unlink")
|
|
193
|
-
.description("Revoke this folder's token and remove local .getmyenv files")
|
|
194
|
-
.option("--yes", "Do not ask for confirmation")
|
|
195
|
-
.option(...API_OPTION)
|
|
196
|
-
.action((opts) => unlinkCommand(opts));
|
|
81
|
+
define(program, "status").action((opts) => statusCommand(opts));
|
|
82
|
+
const context = define(program, "context", false);
|
|
83
|
+
define(context, "context list").action((opts) => contextListCommand(opts));
|
|
84
|
+
define(context, "context create").action((name, opts) => contextCreateCommand(name, opts));
|
|
85
|
+
define(context, "context use").action((slug, opts) => contextUseCommand(slug, opts));
|
|
86
|
+
define(program, "set").action((pairs, opts) => setCommand(pairs, opts));
|
|
87
|
+
define(program, "unset").action((names, opts) => unsetCommand(names, opts));
|
|
88
|
+
define(program, "ls").action((opts) => lsCommand(opts));
|
|
89
|
+
define(program, "import").action((file, opts) => importCommand(file, opts));
|
|
90
|
+
define(program, "export").action((opts) => exportCommand(opts));
|
|
91
|
+
define(program, "guest").action((opts) => guestCommand(opts));
|
|
92
|
+
define(program, "claim").action((opts) => claimCommand(opts));
|
|
93
|
+
define(program, "backup").action((opts) => backupCommand(opts));
|
|
94
|
+
define(program, "restore").action((file, opts) => restoreCommand(file, opts));
|
|
95
|
+
define(program, "lock").action((opts) => lockCommand(opts));
|
|
96
|
+
define(program, "unlink").action((opts) => unlinkCommand(opts));
|
|
97
|
+
return program;
|
|
98
|
+
}
|
|
99
|
+
export async function runCli(argv) {
|
|
100
|
+
const program = buildProgram(argv);
|
|
197
101
|
const first = argv[2];
|
|
198
102
|
if (first &&
|
|
199
103
|
!first.startsWith("-") &&
|
package/dist/commands/claim.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
+
import { GUEST_GRACE_DAYS } from "@getmyenv/shared";
|
|
2
3
|
import { createUserKeys, openSealedKey, sealContextKey, unwrapPrivateKey, wipe, } from "@getmyenv/crypto";
|
|
3
4
|
import { clearClaimFile, ensureGitignoreHasEnv, promptHidden, readClaimFile, readLine, readProjectMeta, writeClaimFile, writeProjectMeta, } from "../lib/fs.js";
|
|
4
5
|
import { readFolderToken, writeFolderToken } from "../lib/credentials.js";
|
|
@@ -136,7 +137,7 @@ export async function claimCommand(opts = {}) {
|
|
|
136
137
|
throw new Error("This claim code is not valid. Check .getmyenv/claim.json or the code shown at creation.");
|
|
137
138
|
}
|
|
138
139
|
if (start.status === "expired") {
|
|
139
|
-
throw new Error(
|
|
140
|
+
throw new Error(`This guest project expired more than ${GUEST_GRACE_DAYS} days ago. It can no longer be claimed.`);
|
|
140
141
|
}
|
|
141
142
|
if (start.status === "claimed") {
|
|
142
143
|
await resumeClaim(apiUrl, claim);
|
package/dist/commands/import.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
+
import { FACTS } from "@getmyenv/shared";
|
|
4
5
|
import { contextSlugFromEnvFile, ensureGitignoreHasEnv, parseEnvFile, promptConfirm, rootEnvFiles, } from "../lib/fs.js";
|
|
5
6
|
import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
6
7
|
import { defaultContextSlug, findContext, resolveContext } from "../lib/context.js";
|
|
@@ -103,7 +104,7 @@ export async function importCommand(file, opts) {
|
|
|
103
104
|
continue;
|
|
104
105
|
}
|
|
105
106
|
if (ctx.protection === "locked") {
|
|
106
|
-
console.error(pc.yellow(`Skipped ${label}: ${ctx.slug} is read-only.
|
|
107
|
+
console.error(pc.yellow(`Skipped ${label}: ${ctx.slug} is read-only. ${FACTS.readOnlyWho}`));
|
|
107
108
|
skipped++;
|
|
108
109
|
continue;
|
|
109
110
|
}
|
package/dist/commands/start.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
-
import { DEFAULT_CONTEXT_SLUG } from "@getmyenv/shared";
|
|
4
|
+
import { DEFAULT_CONTEXT_SLUG, GUEST_TOTAL_DAYS } from "@getmyenv/shared";
|
|
5
5
|
import { promptChoice, promptConfirm, readClaimFile, readLine, readProjectMeta, rootEnvFiles, writeProjectMeta, } from "../lib/fs.js";
|
|
6
6
|
import { ApiClient, getSessionToken, resolveApiUrl } from "../lib/api.js";
|
|
7
7
|
import { authorizeFolder } from "../lib/authorize.js";
|
|
@@ -80,7 +80,7 @@ export async function startSetup(opts) {
|
|
|
80
80
|
}
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
-
const choice = await promptChoice("How do you want to start?", [
|
|
83
|
+
const choice = await promptChoice("How do you want to start?", [`Start without an account (guest project, claim within ${GUEST_TOTAL_DAYS} days)`, "Sign in"], 0);
|
|
84
84
|
console.error("");
|
|
85
85
|
let context = DEFAULT_CONTEXT_SLUG;
|
|
86
86
|
let dashboardUrl = null;
|
package/dist/lib/bootstrap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
+
import { GUEST_GRACE_MS } from "@getmyenv/shared";
|
|
2
3
|
import { ensureGitignoreHasEnv, readClaimFile, readProjectMeta, writeProjectMeta, } from "./fs.js";
|
|
3
4
|
import { ApiClient, ApiError, getSessionToken, resolveApiUrl, } from "./api.js";
|
|
4
5
|
import { assertTokenNotCommitted } from "./credentials.js";
|
|
@@ -14,9 +15,9 @@ export function printGuestBanner(who) {
|
|
|
14
15
|
const DAY = 24 * 60 * 60 * 1000;
|
|
15
16
|
const expires = who.guestExpiresAt ? new Date(who.guestExpiresAt).getTime() : NaN;
|
|
16
17
|
const needClaim = readClaimFile() ? "" : " Needs the claim code and guest passphrase shown at creation.";
|
|
17
|
-
const plural = (n) =>
|
|
18
|
+
const plural = (n) => `${n} day${n === 1 ? "" : "s"}`;
|
|
18
19
|
if (Number.isFinite(expires) && expires <= Date.now()) {
|
|
19
|
-
const graceLeft = Math.max(0, Math.ceil((expires +
|
|
20
|
+
const graceLeft = Math.max(0, Math.ceil((expires + GUEST_GRACE_MS - Date.now()) / DAY));
|
|
20
21
|
console.error(pc.yellow(`Guest project expired. Values can't change. run works for ${plural(graceLeft)} more. Export or claim it: npx getmyenv claim.${needClaim}\n`));
|
|
21
22
|
return;
|
|
22
23
|
}
|
package/dist/lib/fs.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import dotenv from "dotenv";
|
|
6
|
-
import { CONTEXT_SLUG_RE, isValidContextSlug, isValidVariableName, LOCAL_DIR, slugify, } from "@getmyenv/shared";
|
|
6
|
+
import { CONTEXT_SLUG_RE, isValidContextSlug, isValidVariableName, LOCAL_DIR, VAULT_UNLOCK_DURATION, slugify, } from "@getmyenv/shared";
|
|
7
7
|
const SLUG_RE = CONTEXT_SLUG_RE;
|
|
8
8
|
/**
|
|
9
9
|
* Nearest folder at or above cwd with a linked `.getmyenv/`, so commands work
|
|
@@ -142,7 +142,7 @@ export function readLine(question) {
|
|
|
142
142
|
export async function promptHidden(question) {
|
|
143
143
|
const stdin = process.stdin;
|
|
144
144
|
if (!stdin.isTTY) {
|
|
145
|
-
throw new Error(
|
|
145
|
+
throw new Error(`No terminal to ask for the Vault password. Unlock once in a terminal: open contexts stay unlocked for ${VAULT_UNLOCK_DURATION}. On servers and in CI, use a server token.`);
|
|
146
146
|
}
|
|
147
147
|
// Prompts go to stderr so stdout stays clean for `export --stdout > .env` and pipes.
|
|
148
148
|
const out = process.stderr;
|
package/dist/lib/keychain.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { VAULT_UNLOCK_MS } from "@getmyenv/shared";
|
|
2
3
|
/**
|
|
3
4
|
* Unlocked open context keys, remembered in the OS keychain (macOS Keychain,
|
|
4
5
|
* Windows Credential Manager, Linux Secret Service) for a few hours so `run`
|
|
@@ -6,7 +7,7 @@ import { createHash } from "node:crypto";
|
|
|
6
7
|
* Never the Vault password or the private key.
|
|
7
8
|
*/
|
|
8
9
|
const SERVICE = "getmyenv";
|
|
9
|
-
export const KEYCHAIN_TTL_MS =
|
|
10
|
+
export const KEYCHAIN_TTL_MS = VAULT_UNLOCK_MS;
|
|
10
11
|
let factoryOverride;
|
|
11
12
|
let accountsOverride;
|
|
12
13
|
/** Tests swap in an in-memory keyring. `null` means no keyring available. */
|
package/dist/lib/upload.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { encryptSecret, serializePayload } from "@getmyenv/crypto";
|
|
2
|
+
import { FACTS } from "@getmyenv/shared";
|
|
2
3
|
import { contextKey } from "./vault.js";
|
|
3
4
|
/** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
|
|
4
5
|
export function assertWritable(linked, ctx) {
|
|
5
6
|
if (linked.who.token.kind === "server") {
|
|
6
|
-
throw new Error(
|
|
7
|
+
throw new Error(`Server tokens are read-only. ${FACTS.readOnlyWho}`);
|
|
7
8
|
}
|
|
8
9
|
if (ctx.protection === "locked") {
|
|
9
|
-
throw new Error(`${ctx.slug} is read-only.
|
|
10
|
+
throw new Error(`${ctx.slug} is read-only. ${FACTS.readOnlyWho}`);
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
/**
|
package/dist/lib/vault.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Linked } from "./bootstrap.js";
|
|
|
7
7
|
* Folder and guest tokens: the account (or guest) private key is wrapped with
|
|
8
8
|
* the Vault password (or the guest passphrase in claim.json). It is unwrapped
|
|
9
9
|
* here, opens the sealed context keys, and is wiped. Open context keys may
|
|
10
|
-
* stay in the OS keychain for
|
|
10
|
+
* stay in the OS keychain for VAULT_UNLOCK_HOURS. The private key is never stored.
|
|
11
11
|
*
|
|
12
12
|
* Server tokens: GETMYENV_TOKEN holds the token's own private key. It opens
|
|
13
13
|
* the one context key sealed to that token. No Vault password.
|
package/dist/lib/vault.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import { createUserKeys, generateContextKey, openSealedKey, sealContextKey, splitServerToken, unwrapPrivateKey, wipe, } from "@getmyenv/crypto";
|
|
3
|
+
import { VAULT_UNLOCK_DURATION } from "@getmyenv/shared";
|
|
3
4
|
import { ApiError } from "./api.js";
|
|
4
5
|
import { resolveVaultPassword } from "./guest-key.js";
|
|
5
|
-
import {
|
|
6
|
+
import { keychainAccount, keychainEnabled, readCachedContextKey, storeCachedContextKeys, } from "./keychain.js";
|
|
6
7
|
/** One unlock per process: several contexts in one command prompt once. */
|
|
7
8
|
const memo = new Map();
|
|
8
9
|
export function fetchVault(linked) {
|
|
@@ -92,7 +93,7 @@ async function remember(account, vault, opened) {
|
|
|
92
93
|
if (entries.length === 0)
|
|
93
94
|
return;
|
|
94
95
|
if (await storeCachedContextKeys(account, entries)) {
|
|
95
|
-
console.error(pc.dim(`Unlocked on this machine for ${
|
|
96
|
+
console.error(pc.dim(`Unlocked on this machine for ${VAULT_UNLOCK_DURATION} (OS keychain). Forget it with: npx getmyenv lock\n`));
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
function openAll(vault, privateKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getmyenv",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "CLI for getmyenv, an encrypted store for environment variables and secrets. We store ciphertext only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@getmyenv/crypto": "0.4.
|
|
37
|
-
"@getmyenv/shared": "0.
|
|
36
|
+
"@getmyenv/crypto": "0.4.1",
|
|
37
|
+
"@getmyenv/shared": "0.7.0",
|
|
38
38
|
"@napi-rs/keyring": "^2.1.0",
|
|
39
39
|
"commander": "^15.0.0",
|
|
40
40
|
"dotenv": "^16.4.7",
|