getmyenv 0.7.0 → 0.9.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.
Files changed (138) hide show
  1. package/README.md +19 -17
  2. package/dist/bin.d.ts +0 -1
  3. package/dist/bin.js +2 -2
  4. package/dist/cli.d.ts +0 -1
  5. package/dist/cli.js +13 -17
  6. package/dist/commands/backup.d.ts +1 -2
  7. package/dist/commands/backup.js +11 -7
  8. package/dist/commands/claim.d.ts +1 -2
  9. package/dist/commands/claim.js +84 -40
  10. package/dist/commands/context.d.ts +0 -1
  11. package/dist/commands/context.js +23 -7
  12. package/dist/commands/export.d.ts +5 -3
  13. package/dist/commands/export.js +21 -20
  14. package/dist/commands/guest.d.ts +2 -3
  15. package/dist/commands/guest.js +10 -11
  16. package/dist/commands/import.d.ts +2 -2
  17. package/dist/commands/import.js +6 -4
  18. package/dist/commands/lock.d.ts +1 -2
  19. package/dist/commands/lock.js +5 -9
  20. package/dist/commands/ls.d.ts +0 -1
  21. package/dist/commands/ls.js +0 -1
  22. package/dist/commands/restore.d.ts +3 -4
  23. package/dist/commands/restore.js +52 -28
  24. package/dist/commands/run.d.ts +0 -2
  25. package/dist/commands/run.js +9 -62
  26. package/dist/commands/set.d.ts +0 -2
  27. package/dist/commands/set.js +4 -17
  28. package/dist/commands/start.d.ts +8 -3
  29. package/dist/commands/start.js +49 -11
  30. package/dist/commands/status.d.ts +0 -1
  31. package/dist/commands/status.js +3 -2
  32. package/dist/commands/template.d.ts +13 -0
  33. package/dist/commands/template.js +75 -0
  34. package/dist/commands/unlink.d.ts +0 -1
  35. package/dist/commands/unlink.js +5 -9
  36. package/dist/lib/api-url.d.ts +0 -1
  37. package/dist/lib/api-url.js +0 -1
  38. package/dist/lib/api.d.ts +13 -8
  39. package/dist/lib/api.js +10 -5
  40. package/dist/lib/authorize.d.ts +0 -1
  41. package/dist/lib/authorize.js +4 -5
  42. package/dist/lib/bootstrap.d.ts +0 -1
  43. package/dist/lib/bootstrap.js +6 -4
  44. package/dist/lib/confirm.d.ts +0 -1
  45. package/dist/lib/confirm.js +0 -1
  46. package/dist/lib/context.d.ts +3 -2
  47. package/dist/lib/context.js +4 -2
  48. package/dist/lib/credentials.d.ts +4 -1
  49. package/dist/lib/credentials.js +28 -2
  50. package/dist/lib/fs.d.ts +10 -7
  51. package/dist/lib/fs.js +37 -22
  52. package/dist/lib/guest-key.d.ts +7 -4
  53. package/dist/lib/guest-key.js +22 -30
  54. package/dist/lib/identity.d.ts +1 -2
  55. package/dist/lib/identity.js +1 -2
  56. package/dist/lib/keychain.d.ts +19 -6
  57. package/dist/lib/keychain.js +69 -19
  58. package/dist/lib/known-keys.d.ts +3 -0
  59. package/dist/lib/known-keys.js +17 -0
  60. package/dist/lib/resolve.d.ts +25 -0
  61. package/dist/lib/resolve.js +56 -0
  62. package/dist/lib/run-env.d.ts +2 -4
  63. package/dist/lib/run-env.js +9 -1
  64. package/dist/lib/update-check.d.ts +0 -1
  65. package/dist/lib/update-check.js +4 -2
  66. package/dist/lib/upload.d.ts +0 -2
  67. package/dist/lib/upload.js +11 -20
  68. package/dist/lib/vault.d.ts +54 -0
  69. package/dist/lib/vault.js +174 -0
  70. package/package.json +6 -7
  71. package/dist/bin.d.ts.map +0 -1
  72. package/dist/bin.js.map +0 -1
  73. package/dist/cli.d.ts.map +0 -1
  74. package/dist/cli.js.map +0 -1
  75. package/dist/commands/backup.d.ts.map +0 -1
  76. package/dist/commands/backup.js.map +0 -1
  77. package/dist/commands/claim.d.ts.map +0 -1
  78. package/dist/commands/claim.js.map +0 -1
  79. package/dist/commands/context.d.ts.map +0 -1
  80. package/dist/commands/context.js.map +0 -1
  81. package/dist/commands/export.d.ts.map +0 -1
  82. package/dist/commands/export.js.map +0 -1
  83. package/dist/commands/guest.d.ts.map +0 -1
  84. package/dist/commands/guest.js.map +0 -1
  85. package/dist/commands/import.d.ts.map +0 -1
  86. package/dist/commands/import.js.map +0 -1
  87. package/dist/commands/lock.d.ts.map +0 -1
  88. package/dist/commands/lock.js.map +0 -1
  89. package/dist/commands/ls.d.ts.map +0 -1
  90. package/dist/commands/ls.js.map +0 -1
  91. package/dist/commands/restore.d.ts.map +0 -1
  92. package/dist/commands/restore.js.map +0 -1
  93. package/dist/commands/run.d.ts.map +0 -1
  94. package/dist/commands/run.js.map +0 -1
  95. package/dist/commands/set.d.ts.map +0 -1
  96. package/dist/commands/set.js.map +0 -1
  97. package/dist/commands/start.d.ts.map +0 -1
  98. package/dist/commands/start.js.map +0 -1
  99. package/dist/commands/status.d.ts.map +0 -1
  100. package/dist/commands/status.js.map +0 -1
  101. package/dist/commands/unlink.d.ts.map +0 -1
  102. package/dist/commands/unlink.js.map +0 -1
  103. package/dist/lib/api-url.d.ts.map +0 -1
  104. package/dist/lib/api-url.js.map +0 -1
  105. package/dist/lib/api.d.ts.map +0 -1
  106. package/dist/lib/api.js.map +0 -1
  107. package/dist/lib/authorize.d.ts.map +0 -1
  108. package/dist/lib/authorize.js.map +0 -1
  109. package/dist/lib/bootstrap.d.ts.map +0 -1
  110. package/dist/lib/bootstrap.js.map +0 -1
  111. package/dist/lib/confirm.d.ts.map +0 -1
  112. package/dist/lib/confirm.js.map +0 -1
  113. package/dist/lib/context.d.ts.map +0 -1
  114. package/dist/lib/context.js.map +0 -1
  115. package/dist/lib/credentials.d.ts.map +0 -1
  116. package/dist/lib/credentials.js.map +0 -1
  117. package/dist/lib/fs.d.ts.map +0 -1
  118. package/dist/lib/fs.js.map +0 -1
  119. package/dist/lib/guest-key.d.ts.map +0 -1
  120. package/dist/lib/guest-key.js.map +0 -1
  121. package/dist/lib/identity.d.ts.map +0 -1
  122. package/dist/lib/identity.js.map +0 -1
  123. package/dist/lib/keychain.d.ts.map +0 -1
  124. package/dist/lib/keychain.js.map +0 -1
  125. package/dist/lib/pek.d.ts +0 -27
  126. package/dist/lib/pek.d.ts.map +0 -1
  127. package/dist/lib/pek.js +0 -60
  128. package/dist/lib/pek.js.map +0 -1
  129. package/dist/lib/run-env.d.ts.map +0 -1
  130. package/dist/lib/run-env.js.map +0 -1
  131. package/dist/lib/unlock.d.ts +0 -24
  132. package/dist/lib/unlock.d.ts.map +0 -1
  133. package/dist/lib/unlock.js +0 -83
  134. package/dist/lib/unlock.js.map +0 -1
  135. package/dist/lib/update-check.d.ts.map +0 -1
  136. package/dist/lib/update-check.js.map +0 -1
  137. package/dist/lib/upload.d.ts.map +0 -1
  138. package/dist/lib/upload.js.map +0 -1
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # getmyenv
2
2
 
3
- CLI for [getmyenv](https://getmyenv.com) - encrypted environment variables for any language.
4
-
5
3
  Keep using environment variables. Stop managing them as files.
6
4
 
5
+ [getmyenv](https://getmyenv.com) is an encrypted store for environment variables and secrets. Use it from the dashboard or with `npx getmyenv`. We store ciphertext only.
6
+
7
7
  ```bash
8
8
  cd your-project
9
9
  npx getmyenv # Start without an account, or Sign in
@@ -22,35 +22,37 @@ npx getmyenv run staging -- npm start # or pick one
22
22
  - `import [file] [--prune] [--dry-run] [--yes]`: `.env.<slug>` goes to that context.
23
23
  - `export [-o file | --stdout] [--yes]`: for tools that need a file. The file is added to `.gitignore` and gets owner-only permissions on macOS and Linux.
24
24
  - `guest [--name <name>]`: a project without an account. It works for 30 days. Claim it within 60.
25
- - `claim`: claim a guest project from `.getmyenv/claim.json`.
26
- - `backup [-o file]`: encrypted backup of open contexts (default `<project>.getmyenv`).
27
- - `restore <file> [--dry-run] [--yes]`
28
- - `lock`, `unlink [--yes]`, `help [command]`
29
-
30
- `run`, `set`, `unset`, `ls`, `import` and `export` take `-c, --context <slug>`. The default is the server token's context, then `context` in `.getmyenv/project.json`, then `development`.
25
+ - `claim`: claim a guest project. Reads `.getmyenv/claim.json`, or asks for the claim code and guest passphrase that `guest` printed once.
26
+ - `backup [-o file]`: encrypted backup of open contexts (default `<project>.getmyenv-backup`). It opens with the Vault password in effect when it was made.
27
+ - `restore <file> [--dry-run] [--yes]`: restore a backup into this folder's project. Values go into open contexts with the same slug and replace values with the same name. Read-only and missing contexts are skipped.
28
+ - `start`: same as bare `getmyenv`. `start --template <id or link>` adds a template's variable names and asks for each value.
29
+ - `lock`: forget every unlocked context key on this machine. `unlink [--yes]`, `help [command]`
31
30
 
32
- `run`, `set`, `import`, `export` and `restore` take `--vault-password-stdin`. Every command takes `--api-url <url>`.
31
+ `run`, `set`, `unset`, `ls`, `import` and `export` take `-c, --context <slug>`. The default is the server token's context, then `context` in `.getmyenv/project.json`, then `development`. Every command takes `--api-url <url>`.
33
32
 
34
33
  Prompts and status lines go to stderr, so `export --stdout > .env` stays clean.
35
34
 
35
+ Commit `.getmyenv/project.json`. `token.json` and `claim.json` stay local and are gitignored.
36
+
36
37
  ## Vault password
37
38
 
38
- For open contexts, after you enter the Vault password in a terminal, the unlocked project key stays in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) for 8 hours. The password itself is never stored.
39
+ 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.
39
40
 
40
- - `npx getmyenv lock` removes it. `unlink` removes it too.
41
- - Read-only contexts, CI and `--vault-password-stdin` ask on each run.
41
+ - `npx getmyenv lock` removes every entry. `unlink` removes this project's.
42
+ - Server tokens hold their context key and never ask.
42
43
  - Guest projects unlock with the passphrase in `.getmyenv/claim.json` and never ask.
43
44
  - `GETMYENV_KEYCHAIN=0` always asks.
44
45
 
45
46
  ## CI and servers
46
47
 
48
+ Create a server token for one context on the dashboard CLI page. It holds that context's key.
49
+
47
50
  ```bash
48
- printf %s "$VAULT_PASSWORD" | npx getmyenv run --vault-password-stdin -- node server.js
49
- printf %s "$VALUE" | npx getmyenv set API_KEY --stdin
50
- printf '%s\n%s' "$VAULT_PASSWORD" "$VALUE" | npx getmyenv set API_KEY --stdin --vault-password-stdin
51
+ export GETMYENV_TOKEN=... # server token from the dashboard
52
+ npx getmyenv run -- node server.js
51
53
  ```
52
54
 
53
- Server tokens are read-only, so `set` needs the folder token from browser sign-in (or a guest project). With both `--stdin` and `--vault-password-stdin`, send the password on the first line and the value after it.
55
+ Server tokens are read-only. `set` needs the folder token from browser sign-in, or a guest project. `printf %s "$VALUE" | npx getmyenv set API_KEY --stdin` keeps the value out of argv.
54
56
 
55
57
  - `GETMYENV_TOKEN`: a token from the dashboard CLI page.
56
58
  - `GETMYENV_CONFIG_DIR`: machine key and cache (default `~/.config/getmyenv`). Use a persistent volume in containers.
@@ -63,7 +65,7 @@ Server tokens are read-only, so `set` needs the folder token from browser sign-i
63
65
  ## Open and Read-only
64
66
 
65
67
  - Open contexts: the CLI can read and write. Browser sign-in gives the folder a token in `.getmyenv/token.json`.
66
- - Read-only contexts: the CLI can only read. Set values in the dashboard. Use a server token from the CLI page (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.
68
+ - Read-only contexts: the CLI can read and export, never write. Set 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.
67
69
 
68
70
  ## Chaining
69
71
 
package/dist/bin.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=bin.d.ts.map
package/dist/bin.js CHANGED
@@ -9,6 +9,6 @@ main().catch((err) => {
9
9
  const message = err instanceof Error ? err.message : "Unexpected error";
10
10
  // Never print secret values - sanitize to message only
11
11
  console.error(`getmyenv: ${message}`);
12
- process.exit(process.exitCode ? Number(process.exitCode) : 1);
12
+ // Not process.exit(): on Windows it can abort while sockets are still closing.
13
+ process.exitCode = process.exitCode ? Number(process.exitCode) : 1;
13
14
  });
14
- //# sourceMappingURL=bin.js.map
package/dist/cli.d.ts CHANGED
@@ -9,4 +9,3 @@ export declare function splitRunArgs(operands: string[], argv: string[], explain
9
9
  command: string[];
10
10
  };
11
11
  export declare function runCli(argv: string[]): Promise<void>;
12
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -16,13 +16,9 @@ import { unlinkCommand } from "./commands/unlink.js";
16
16
  import { CLI_VERSION } from "./lib/api.js";
17
17
  const CONTEXT_OPTION = [
18
18
  "-c, --context <slug>",
19
- "Context to use (default: project.json context, then development)",
19
+ "Context to use (default: the server token's context, then project.json, then development)",
20
20
  ];
21
21
  const API_OPTION = ["--api-url <url>", "API base URL"];
22
- const STDIN_OPTION = [
23
- "--vault-password-stdin",
24
- "Read the Vault password from stdin (non-interactive)",
25
- ];
26
22
  /**
27
23
  * Split `run` operands around `--`.
28
24
  * `run staging -- npm start`: context staging, command npm start.
@@ -50,7 +46,7 @@ export async function runCli(argv) {
50
46
  .addHelpText("after", [
51
47
  "",
52
48
  "Environment:",
53
- " GETMYENV_TOKEN Token for CI and servers (instead of .getmyenv/token.json)",
49
+ " GETMYENV_TOKEN Token for CI and servers (instead of .getmyenv/token.json). A server token holds its context key.",
54
50
  " GETMYENV_API_URL API base URL (https, or http://localhost)",
55
51
  " GETMYENV_CONFIG_DIR Machine identity and cache dir (default ~/.config/getmyenv)",
56
52
  " GETMYENV_SKIP_UPDATE Set to 1 to skip the update notice",
@@ -66,6 +62,12 @@ export async function runCli(argv) {
66
62
  actionCommand.setOptionValue("apiUrl", apiUrl);
67
63
  }
68
64
  });
65
+ program
66
+ .command("start")
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));
69
71
  program
70
72
  .command("run")
71
73
  .description("Run a command with a context's variables in its environment")
@@ -75,14 +77,13 @@ export async function runCli(argv) {
75
77
  "Examples:",
76
78
  " npx getmyenv run -- npm run dev",
77
79
  " npx getmyenv run staging -- node server.js",
78
- ' printf %s "$VAULT_PASSWORD" | npx getmyenv run production --vault-password-stdin -- node server.js',
80
+ " GETMYENV_TOKEN=<server token> npx getmyenv run production -- node server.js",
79
81
  ].join("\n"))
80
82
  .argument("[context]", "Context slug (same as -c)")
81
83
  .option(...CONTEXT_OPTION)
82
84
  .option("--explain", "List each variable and where its value comes from (names only)")
83
85
  .option("--keep-existing", "Keep variables already set in the parent environment")
84
86
  .option("--allow-missing", "Run even when some variables have no value in this context")
85
- .option(...STDIN_OPTION)
86
87
  .option(...API_OPTION)
87
88
  .allowUnknownOption(true)
88
89
  .allowExcessArguments(true)
@@ -123,7 +124,6 @@ export async function runCli(argv) {
123
124
  .option(...CONTEXT_OPTION)
124
125
  .option("--expires <date>", "Expiry date, e.g. 2026-12-31, or never")
125
126
  .option("--stdin", "Read the value of one KEY from stdin (keeps it out of argv)")
126
- .option(...STDIN_OPTION)
127
127
  .option(...API_OPTION)
128
128
  .action((pairs, opts) => setCommand(pairs, opts));
129
129
  program
@@ -148,7 +148,6 @@ export async function runCli(argv) {
148
148
  .option("--prune", "Unset values in the context that are not in the file")
149
149
  .option("--dry-run", "Show what would change without uploading")
150
150
  .option("--yes", "Do not ask for confirmation")
151
- .option(...STDIN_OPTION)
152
151
  .option(...API_OPTION)
153
152
  .action((file, opts) => importCommand(file, opts));
154
153
  program
@@ -158,7 +157,6 @@ export async function runCli(argv) {
158
157
  .option("-o, --output <file>", "Output file", ".env")
159
158
  .option("--stdout", "Write to stdout instead of a file")
160
159
  .option("--yes", "Change an existing file without asking")
161
- .option(...STDIN_OPTION)
162
160
  .option(...API_OPTION)
163
161
  .action((opts) => exportCommand(opts));
164
162
  program
@@ -169,26 +167,25 @@ export async function runCli(argv) {
169
167
  .action((opts) => guestCommand(opts));
170
168
  program
171
169
  .command("claim")
172
- .description("Claim a guest project from .getmyenv/claim.json")
170
+ .description("Claim a guest project (reads .getmyenv/claim.json, or asks for the claim code)")
173
171
  .option(...API_OPTION)
174
172
  .action((opts) => claimCommand(opts));
175
173
  program
176
174
  .command("backup")
177
175
  .description("Write an encrypted backup of open contexts")
178
- .option("-o, --output <file>", "Output file (default: <project>.getmyenv)")
176
+ .option("-o, --output <file>", "Output file (default: <project>.getmyenv-backup)")
179
177
  .option(...API_OPTION)
180
178
  .action((opts) => backupCommand(opts));
181
179
  program
182
180
  .command("restore <file>")
183
- .description("Restore an encrypted backup into this folder's project")
181
+ .description("Restore a backup into this folder's project (open contexts with the same slug)")
184
182
  .option("--dry-run", "Check the backup and show what would be restored")
185
183
  .option("--yes", "Do not ask for confirmation")
186
- .option(...STDIN_OPTION)
187
184
  .option(...API_OPTION)
188
185
  .action((file, opts) => restoreCommand(file, opts));
189
186
  program
190
187
  .command("lock")
191
- .description("Forget the unlocked project key on this machine (OS keychain)")
188
+ .description("Forget every unlocked context key on this machine (OS keychain)")
192
189
  .option(...API_OPTION)
193
190
  .action((opts) => lockCommand(opts));
194
191
  program
@@ -206,4 +203,3 @@ export async function runCli(argv) {
206
203
  }
207
204
  await program.parseAsync(argv);
208
205
  }
209
- //# sourceMappingURL=cli.js.map
@@ -2,7 +2,6 @@ type BackupOpts = {
2
2
  output?: string;
3
3
  apiUrl?: string;
4
4
  };
5
- /** Encrypted backup of open contexts. Readable offline with the Vault password. */
5
+ /** Encrypted backup of open contexts. Opens offline with the current Vault password. */
6
6
  export declare function backupCommand(opts: BackupOpts): Promise<void>;
7
7
  export {};
8
- //# sourceMappingURL=backup.d.ts.map
@@ -1,16 +1,20 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import pc from "picocolors";
4
3
  import { createBackupEnvelope, parsePayload, parseWrappedKey, serializeBackup, } from "@getmyenv/crypto";
5
4
  import { slugify } from "@getmyenv/shared";
6
5
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
7
- /** Encrypted backup of open contexts. Readable offline with the Vault password. */
6
+ import { writePrivateFile } from "../lib/fs.js";
7
+ /** Encrypted backup of open contexts. Opens offline with the current Vault password. */
8
8
  export async function backupCommand(opts) {
9
9
  const { client } = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
10
10
  const data = await client.request("GET", "/api/cli/backup");
11
+ if (!data.publicKey || !data.wrappedPrivateKey) {
12
+ throw new Error("No Vault password yet, so there is nothing to back up. Set a value first: npx getmyenv set KEY");
13
+ }
11
14
  const envelope = createBackupEnvelope({
12
15
  projectName: data.projectName,
13
- wrappedProjectKey: parseWrappedKey(JSON.stringify(data.wrappedProjectKey)),
16
+ publicKey: data.publicKey,
17
+ wrappedPrivateKey: parseWrappedKey(JSON.stringify(data.wrappedPrivateKey)),
14
18
  contexts: data.contexts,
15
19
  keys: data.keys,
16
20
  tags: data.tags,
@@ -21,9 +25,9 @@ export async function backupCommand(opts) {
21
25
  payload: parsePayload(v.ciphertext),
22
26
  })),
23
27
  });
24
- const out = opts.output ?? path.join(process.cwd(), `${slugify(data.projectName) || "project"}.getmyenv`);
25
- fs.writeFileSync(out, serializeBackup(envelope), { mode: 0o600 });
28
+ const out = opts.output ??
29
+ path.join(process.cwd(), `${slugify(data.projectName) || "project"}.getmyenv-backup`);
30
+ writePrivateFile(out, serializeBackup(envelope), { overwrite: false });
26
31
  console.log(`${pc.green("✓")} Encrypted backup: ${out} (${data.values.length} values, ${data.contexts.length} open contexts)`);
27
- console.log(pc.dim("Read-only contexts are backed up from the dashboard."));
32
+ console.log(pc.dim("It opens with your current Vault password. Read-only contexts are backed up from the dashboard."));
28
33
  }
29
- //# sourceMappingURL=backup.js.map
@@ -3,7 +3,6 @@ export type ClaimOpts = {
3
3
  };
4
4
  /**
5
5
  * Claim a guest project: sign in and take ownership in the browser, then
6
- * rewrap the project key with a Vault password.
6
+ * hand the context keys to the owner's account.
7
7
  */
8
8
  export declare function claimCommand(opts?: ClaimOpts): Promise<void>;
9
- //# sourceMappingURL=claim.d.ts.map
@@ -1,35 +1,69 @@
1
1
  import pc from "picocolors";
2
- import { parseWrappedKey, rewrapProjectKey, serializeWrappedKey, } from "@getmyenv/crypto";
3
- import { clearClaimFile, ensureGitignoreHasEnv, promptHidden, readClaimFile, readProjectMeta, writeProjectMeta, } from "../lib/fs.js";
2
+ import { createUserKeys, openSealedKey, sealContextKey, unwrapPrivateKey, wipe, } from "@getmyenv/crypto";
3
+ import { clearClaimFile, ensureGitignoreHasEnv, promptHidden, readClaimFile, readLine, readProjectMeta, writeClaimFile, writeProjectMeta, } from "../lib/fs.js";
4
4
  import { readFolderToken, writeFolderToken } from "../lib/credentials.js";
5
- import { writeCachedKey } from "../lib/pek.js";
6
5
  import { ApiClient, ApiError, resolveApiUrl } from "../lib/api.js";
7
6
  import { authorizeFolder, openBrowser, sleep } from "../lib/authorize.js";
7
+ import { promptNewVaultPassword } from "../lib/guest-key.js";
8
8
  async function askNewVaultPassword() {
9
- console.error(pc.dim("Choose a Vault password. It stays on this machine. We store an encrypted key only.\n"));
10
- const password = await promptHidden("New Vault password (hidden): ");
11
- if (!password)
12
- throw new Error("Vault password required to claim.");
13
- const confirm = await promptHidden("Confirm Vault password (hidden): ");
14
- if (password !== confirm)
15
- throw new Error("Passwords did not match. Run claim again.");
16
- return password;
9
+ console.error(pc.dim("Choose a Vault password. It stays on this machine. We store an encrypted key blob only.\n"));
10
+ try {
11
+ return await promptNewVaultPassword();
12
+ }
13
+ catch (err) {
14
+ throw new Error(`${err instanceof Error ? err.message : "Vault password required."} Run claim again.`);
15
+ }
17
16
  }
18
- /** Swap the guest-passphrase wrap for a Vault password wrap. Ciphertext stays the same. */
19
- async function finishRewrap(owned, claim, password) {
20
- const { wrappedProjectKey } = await owned.request("GET", "/api/cli/vault");
21
- if (wrappedProjectKey != null) {
22
- const rewrapped = serializeWrappedKey(await rewrapProjectKey(claim.passphrase, password, parseWrappedKey(JSON.stringify(wrappedProjectKey))));
23
- await owned.request("POST", "/api/cli/vault", {
24
- wrappedProjectKey: JSON.parse(rewrapped),
17
+ /**
18
+ * Hand the context keys to the owner. The guest key opens them with the
19
+ * claim.json passphrase, they are sealed to the owner's public key, and the
20
+ * guest keys are deleted. Values stay as they are.
21
+ */
22
+ async function finishClaim(owned, claim) {
23
+ const vault = await owned.request("GET", "/api/cli/vault");
24
+ if (!vault.claim) {
25
+ clearClaimFile();
26
+ console.error(`${pc.green("✓")} ${pc.bold(claim.projectName)} is already set up. claim.json was removed.\n`);
27
+ return;
28
+ }
29
+ let guestKey;
30
+ try {
31
+ guestKey = await unwrapPrivateKey(claim.passphrase, vault.claim.wrappedPrivateKey);
32
+ }
33
+ catch {
34
+ throw new Error("The guest passphrase does not open this project. Delete .getmyenv/claim.json and run claim again with the passphrase shown at creation.");
35
+ }
36
+ let ownerPublicKey = vault.publicKey;
37
+ let keys;
38
+ try {
39
+ if (!ownerPublicKey) {
40
+ const created = await createUserKeys(await askNewVaultPassword());
41
+ wipe(created.privateKey);
42
+ ownerPublicKey = created.publicKey;
43
+ keys = { publicKey: created.publicKey, wrappedPrivateKey: created.wrapped };
44
+ }
45
+ const shares = vault.claim.shares.map((s) => {
46
+ const key = openSealedKey(s.sealedKey, guestKey, s.contextId);
47
+ try {
48
+ return { contextId: s.contextId, sealedKey: sealContextKey(key, ownerPublicKey, s.contextId) };
49
+ }
50
+ finally {
51
+ wipe(key);
52
+ }
53
+ });
54
+ await owned.request("POST", "/api/cli/vault/claim", {
25
55
  claimCode: claim.claimCode,
56
+ ...(keys ? { keys } : {}),
57
+ shares,
26
58
  });
27
- writeCachedKey(rewrapped);
59
+ }
60
+ finally {
61
+ wipe(guestKey);
28
62
  }
29
63
  clearClaimFile();
30
64
  ensureGitignoreHasEnv();
31
- console.error(`\n${pc.green("✓")} Claimed ${pc.bold(claim.projectName)}. Vault password set.`);
32
- console.error(pc.dim("claim.json was removed. Commit that change. Production (read-only) is now available in the dashboard.\n"));
65
+ console.error(`\n${pc.green("✓")} Claimed ${pc.bold(claim.projectName)}. ${keys ? "Vault password set." : "It opens with your Vault password."}`);
66
+ console.error(pc.dim("claim.json was removed. Read-only contexts like Production are now available in the dashboard.\n"));
33
67
  }
34
68
  /** A signed-in folder token for the claimed project: reuse this folder's, or sign in. */
35
69
  async function ownerClient(apiUrl, claim) {
@@ -58,37 +92,48 @@ async function ownerClient(apiUrl, claim) {
58
92
  }
59
93
  /** Claimed in the browser, but the CLI never finished. Finish with claim.json. */
60
94
  async function resumeClaim(apiUrl, claim) {
61
- console.error(pc.dim("This project is already claimed. Finishing the Vault password setup.\n"));
62
- const owned = await ownerClient(apiUrl, claim);
63
- const vault = await owned.request("GET", "/api/cli/vault");
64
- if (!vault.rewrapPending) {
65
- clearClaimFile();
66
- console.error(`${pc.green("✓")} ${pc.bold(claim.projectName)} is already set up. claim.json was removed.\n`);
67
- return;
95
+ console.error(pc.dim("This project is already claimed. Handing its keys to your account.\n"));
96
+ await finishClaim(await ownerClient(apiUrl, claim), claim);
97
+ }
98
+ /** No claim.json here: ask for the claim code and guest passphrase printed when the project was created. */
99
+ async function askClaimDetails() {
100
+ const meta = readProjectMeta();
101
+ if (!meta?.projectId) {
102
+ throw new Error("No .getmyenv/claim.json or project.json in this folder. Run claim in the folder that created the guest project.");
68
103
  }
69
- const password = await askNewVaultPassword();
70
- await finishRewrap(owned, claim, password);
104
+ console.error(pc.dim("No .getmyenv/claim.json here. Enter the claim code and guest passphrase shown when the project was created.\n"));
105
+ const claimCode = (await readLine("Claim code: ")).trim();
106
+ const passphrase = (await promptHidden("Guest passphrase (hidden): ")).trim();
107
+ if (!claimCode || !passphrase)
108
+ throw new Error("Claim code and guest passphrase required.");
109
+ const data = {
110
+ claimCode,
111
+ passphrase,
112
+ projectId: meta.projectId,
113
+ projectName: meta.projectName ?? "this project",
114
+ guestExpiresAt: "",
115
+ };
116
+ ensureGitignoreHasEnv();
117
+ writeClaimFile(data);
118
+ return { version: 1, ...data };
71
119
  }
72
120
  /**
73
121
  * Claim a guest project: sign in and take ownership in the browser, then
74
- * rewrap the project key with a Vault password.
122
+ * hand the context keys to the owner's account.
75
123
  */
76
124
  export async function claimCommand(opts = {}) {
77
125
  console.error(pc.bold("\ngetmyenv claim\n"));
78
- const claim = readClaimFile();
79
- if (!claim) {
80
- throw new Error("No .getmyenv/claim.json in this folder. Run claim in the folder that created the guest project, or in a clone that has claim.json.");
81
- }
82
126
  if (!process.stdin.isTTY) {
83
- throw new Error("Claim needs a terminal to set a Vault password. Run: npx getmyenv claim");
127
+ throw new Error("Claim needs a terminal. Run: npx getmyenv claim");
84
128
  }
129
+ const claim = readClaimFile() ?? (await askClaimDetails());
85
130
  const apiUrl = resolveApiUrl(opts.apiUrl);
86
131
  const client = new ApiClient(apiUrl);
87
132
  const start = await client.request("POST", "/api/claim/start", {
88
133
  claimCode: claim.claimCode,
89
134
  });
90
135
  if (start.status === "invalid") {
91
- throw new Error("This claim code is not valid. Check .getmyenv/claim.json.");
136
+ throw new Error("This claim code is not valid. Check .getmyenv/claim.json or the code shown at creation.");
92
137
  }
93
138
  if (start.status === "expired") {
94
139
  throw new Error("This guest project expired more than 30 days ago. It can no longer be claimed.");
@@ -100,11 +145,11 @@ export async function claimCommand(opts = {}) {
100
145
  if (start.status !== "claimable")
101
146
  throw new Error("Unexpected claim status. Run claim again.");
102
147
  const session = start;
103
- const password = await askNewVaultPassword();
104
148
  const base = apiUrl.replace(/\/$/, "");
105
149
  const claimUrl = `${base}/claim?code=${encodeURIComponent(claim.claimCode)}&session=${encodeURIComponent(session.sessionId)}`;
106
150
  console.error("\nOpen this URL to claim the project:\n");
107
151
  console.error(` ${claimUrl}\n`);
152
+ console.error(`Enter this code on the page: ${pc.bold(session.userCode)}\n`);
108
153
  openBrowser(claimUrl);
109
154
  console.error(pc.dim("Opened in your browser. Waiting...\n"));
110
155
  const deadline = new Date(session.expiresAt).getTime();
@@ -139,6 +184,5 @@ export async function claimCommand(opts = {}) {
139
184
  projectId: claim.projectId,
140
185
  projectName: claim.projectName,
141
186
  });
142
- await finishRewrap(new ApiClient(apiUrl, newToken), claim, password);
187
+ await finishClaim(new ApiClient(apiUrl, newToken), claim);
143
188
  }
144
- //# sourceMappingURL=claim.js.map
@@ -8,4 +8,3 @@ export declare function contextCreateCommand(name: string, opts: Opts & {
8
8
  }): Promise<void>;
9
9
  export declare function contextUseCommand(slug: string, opts: Opts): Promise<void>;
10
10
  export {};
11
- //# sourceMappingURL=context.d.ts.map
@@ -1,7 +1,10 @@
1
1
  import pc from "picocolors";
2
+ import { decryptSecret, parsePayload } from "@getmyenv/crypto";
2
3
  import { readProjectMeta, writeProjectMeta } from "../lib/fs.js";
3
4
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
4
5
  import { defaultContextSlug, findContext } from "../lib/context.js";
6
+ import { uploadValues } from "../lib/upload.js";
7
+ import { contextKey } from "../lib/vault.js";
5
8
  export async function contextListCommand(opts) {
6
9
  const { who, meta } = await ensureTokenAndLink({ apiUrl: opts.apiUrl, quiet: true });
7
10
  const current = defaultContextSlug(who, meta);
@@ -13,19 +16,33 @@ export async function contextListCommand(opts) {
13
16
  }
14
17
  }
15
18
  export async function contextCreateCommand(name, opts) {
16
- const { who, client } = await ensureTokenAndLink({ apiUrl: opts.apiUrl, quiet: true });
17
- let from;
19
+ const linked = await ensureTokenAndLink({ apiUrl: opts.apiUrl, quiet: true });
20
+ const { who, client } = linked;
21
+ let source = null;
18
22
  if (opts.from) {
19
- const source = findContext(who.contexts, opts.from);
23
+ source = findContext(who.contexts, opts.from);
20
24
  if (!source)
21
25
  throw new Error(`No context "${opts.from}".`);
22
26
  if (source.protection === "locked") {
23
27
  throw new Error(`${source.slug} is read-only. Copy from it in the dashboard.`);
24
28
  }
25
- from = source.slug;
26
29
  }
27
- const created = await client.request("POST", "/api/cli/contexts", { name, ...(from ? { from } : {}) });
28
- console.log(`${pc.green("✓")} Context ${pc.bold(created.name)} (${created.slug}), open${from ? `, values copied from ${from}` : ""}`);
30
+ const created = await client.request("POST", "/api/cli/contexts", { name });
31
+ let copied = 0;
32
+ if (source) {
33
+ const data = await client.request("GET", `/api/cli/values?context=${encodeURIComponent(source.slug)}`);
34
+ if (data.values.length > 0) {
35
+ const key = await contextKey(linked, { ...source, sealedKey: data.sealedKey });
36
+ const pairs = Object.fromEntries(data.values.map((v) => [v.name, decryptSecret(key, v.name, parsePayload(v.ciphertext))]));
37
+ const res = await uploadValues({
38
+ linked,
39
+ context: created,
40
+ pairs,
41
+ });
42
+ copied = res.set.length;
43
+ }
44
+ }
45
+ console.log(`${pc.green("✓")} Context ${pc.bold(created.name)} (${created.slug}), open${source ? `, ${copied} value${copied === 1 ? "" : "s"} copied from ${source.slug}` : ""}`);
29
46
  if (opts.use) {
30
47
  const meta = readProjectMeta();
31
48
  writeProjectMeta({ ...(meta ?? {}), context: created.slug });
@@ -50,4 +67,3 @@ export async function contextUseCommand(slug, opts) {
50
67
  console.log(`${pc.green("✓")} Default context for this folder: ${pc.bold(ctx.slug)}`);
51
68
  console.log(pc.dim("Saved in .getmyenv/project.json."));
52
69
  }
53
- //# sourceMappingURL=context.js.map
@@ -4,8 +4,10 @@ export type ExportOpts = {
4
4
  output?: string;
5
5
  stdout?: boolean;
6
6
  yes?: boolean;
7
- vaultPasswordStdin?: boolean;
8
7
  };
9
- /** `export`: write one open context to a plaintext file, for tools that need one. */
8
+ /**
9
+ * `export`: write one context to a plaintext file, for tools that need one.
10
+ * Read-only contexts go through the same checks as `run`: server token,
11
+ * allowed IP, and a trusted or approved machine.
12
+ */
10
13
  export declare function exportCommand(opts: ExportOpts): Promise<void>;
11
- //# sourceMappingURL=export.d.ts.map
@@ -2,33 +2,33 @@ import fs from "node:fs";
2
2
  import pc from "picocolors";
3
3
  import { decryptSecret, parsePayload } from "@getmyenv/crypto";
4
4
  import { EXPORT_WARNING, formatDotenvValue } from "@getmyenv/shared";
5
- import { ensureGitignoreHas, ensureGitignoreHasEnv, parseEnvFile } from "../lib/fs.js";
5
+ import { ensureGitignoreHas, ensureGitignoreHasEnv, parseEnvFile, writePrivateFile, } from "../lib/fs.js";
6
6
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
7
7
  import { resolveContext } from "../lib/context.js";
8
8
  import { confirmDiff } from "../lib/confirm.js";
9
- import { unlockProjectKey } from "../lib/unlock.js";
10
- /** `export`: write one open context to a plaintext file, for tools that need one. */
9
+ import { resolveValues } from "../lib/resolve.js";
10
+ import { contextKey } from "../lib/vault.js";
11
+ import { assertVariableNames } from "../lib/run-env.js";
12
+ /**
13
+ * `export`: write one context to a plaintext file, for tools that need one.
14
+ * Read-only contexts go through the same checks as `run`: server token,
15
+ * allowed IP, and a trusted or approved machine.
16
+ */
11
17
  export async function exportCommand(opts) {
12
18
  const log = opts.stdout ? (l = "") => console.error(l) : (l = "") => console.log(l);
13
19
  const linked = await ensureTokenAndLink({ apiUrl: opts.apiUrl, quiet: opts.stdout });
14
20
  const ctx = resolveContext(linked.who, linked.meta, opts.context);
15
- if (ctx.protection === "locked") {
16
- throw new Error(`${ctx.slug} is read-only. Export it in the dashboard.`);
17
- }
18
21
  log(pc.yellow(EXPORT_WARNING));
19
- const data = await linked.client.request("GET", `/api/cli/values?context=${encodeURIComponent(ctx.slug)}`);
22
+ const data = ctx.protection === "locked"
23
+ ? await resolveValues(linked.client, linked.who.project.id, ctx.slug, "export")
24
+ : await linked.client.request("GET", `/api/cli/values?context=${encodeURIComponent(ctx.slug)}`);
25
+ assertVariableNames([...data.values.map((v) => v.name), ...data.missing.map((m) => m.name)]);
20
26
  const remote = {};
21
27
  if (data.values.length > 0) {
22
- const { pek } = await unlockProjectKey({
23
- isGuest: linked.who.isGuest,
24
- projectId: linked.who.project.id,
25
- wrapped: data.wrappedProjectKey,
26
- cache: true,
27
- vaultPasswordStdin: opts.vaultPasswordStdin,
28
- apiUrl: linked.apiUrl,
29
- });
30
- for (const v of data.values)
31
- remote[v.name] = decryptSecret(pek, parsePayload(v.ciphertext));
28
+ const key = await contextKey(linked, { ...ctx, sealedKey: data.sealedKey });
29
+ for (const v of data.values) {
30
+ remote[v.name] = decryptSecret(key, v.name, parsePayload(v.ciphertext));
31
+ }
32
32
  }
33
33
  const lines = [
34
34
  `# Exported by getmyenv from ${ctx.slug}. Plaintext. Do not commit.`,
@@ -40,6 +40,9 @@ export async function exportCommand(opts) {
40
40
  return;
41
41
  }
42
42
  const out = opts.output || ".env";
43
+ if (fs.lstatSync(out, { throwIfNoEntry: false })?.isSymbolicLink()) {
44
+ throw new Error(`${out} is a symlink. Refusing to write through it.`);
45
+ }
43
46
  if (fs.existsSync(out)) {
44
47
  const local = parseEnvFile(out);
45
48
  const conflicts = Object.keys(remote)
@@ -58,10 +61,8 @@ export async function exportCommand(opts) {
58
61
  return;
59
62
  }
60
63
  }
61
- fs.writeFileSync(out, lines.join("\n") + "\n", { mode: 0o600 });
62
- fs.chmodSync(out, 0o600);
64
+ writePrivateFile(out, lines.join("\n") + "\n", { overwrite: true });
63
65
  ensureGitignoreHasEnv();
64
66
  ensureGitignoreHas(out);
65
67
  log(`${pc.green("✓")} Wrote ${Object.keys(remote).length} variable(s) from ${ctx.slug} to ${out}`);
66
68
  }
67
- //# sourceMappingURL=export.js.map
@@ -5,8 +5,8 @@ export type GuestOpts = {
5
5
  export declare function folderNameHint(): string;
6
6
  /**
7
7
  * Create an unclaimed guest project with one open context, Development.
8
- * Writes token.json and key.json (gitignored), project.json and claim.json (committed).
9
- * claim.json holds the guest passphrase inside the project folder until claim.
8
+ * Writes token.json and claim.json (gitignored) and project.json (committed).
9
+ * claim.json holds the guest passphrase that wraps the guest key until claim.
10
10
  */
11
11
  export declare function createGuestProject(opts?: GuestOpts): Promise<{
12
12
  projectId: string;
@@ -14,4 +14,3 @@ export declare function createGuestProject(opts?: GuestOpts): Promise<{
14
14
  guestExpiresAt: string;
15
15
  }>;
16
16
  export declare function guestCommand(opts?: GuestOpts): Promise<void>;
17
- //# sourceMappingURL=guest.d.ts.map