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
@@ -1,46 +1,38 @@
1
+ import { VAULT_PASSWORD_HELP, validateVaultPassword } from "@getmyenv/shared";
1
2
  import { promptHidden, readClaimFile } from "./fs.js";
3
+ /** Ask for a new Vault password twice. Same rule as the dashboard. */
4
+ export async function promptNewVaultPassword() {
5
+ if (process.stdin.isTTY)
6
+ console.error(VAULT_PASSWORD_HELP);
7
+ const password = await promptHidden("New Vault password (hidden): ");
8
+ if (!password)
9
+ throw new Error("Vault password required.");
10
+ const check = validateVaultPassword(password);
11
+ if (!check.ok)
12
+ throw new Error(check.error ?? VAULT_PASSWORD_HELP);
13
+ const confirm = await promptHidden("Confirm Vault password (hidden): ");
14
+ if (password !== confirm)
15
+ throw new Error("Passwords did not match.");
16
+ return password;
17
+ }
2
18
  /**
3
- * Guest projects wrap the PEK with the passphrase in claim.json.
19
+ * Guest projects wrap the guest private key with the passphrase in claim.json.
4
20
  * Use that silently so guest, set and run need no Vault password prompt.
5
- * Claimed / human projects still prompt for the Vault password.
21
+ * Claimed projects prompt for the Vault password in a terminal.
6
22
  */
7
- let presetPassword = null;
8
- /** Password already read from stdin by the command (for example `set --stdin`). */
9
- export function presetVaultPassword(password) {
10
- presetPassword = password;
11
- }
12
23
  export async function resolveVaultPassword(opts) {
13
- if (!opts.isGuest && presetPassword)
14
- return presetPassword;
15
24
  if (opts.isGuest) {
16
25
  const claim = readClaimFile();
17
26
  if (claim?.passphrase) {
18
27
  return claim.passphrase;
19
28
  }
20
- throw new Error("Guest project is missing .getmyenv/claim.json. Restore it from your repo or a copy.");
21
- }
22
- if (opts.create) {
23
- const password = await promptHidden("New Vault password (hidden): ", {
24
- allowStdin: opts.vaultPasswordStdin,
25
- });
26
- if (!password) {
27
- throw new Error("Vault password required.");
28
- }
29
- if (opts.vaultPasswordStdin) {
30
- return password;
31
- }
32
- const confirm = await promptHidden("Confirm Vault password (hidden): ");
33
- if (password !== confirm) {
34
- throw new Error("Passwords did not match.");
35
- }
36
- return password;
29
+ throw new Error("Guest project is missing .getmyenv/claim.json. Run it from the folder that created the project, or run: npx getmyenv claim");
37
30
  }
38
- const password = await promptHidden(opts.prompt ?? "Vault password (hidden): ", {
39
- allowStdin: opts.vaultPasswordStdin,
40
- });
31
+ if (opts.create)
32
+ return promptNewVaultPassword();
33
+ const password = await promptHidden(opts.prompt ?? "Vault password (hidden): ");
41
34
  if (!password) {
42
35
  throw new Error("Vault password required.");
43
36
  }
44
37
  return password;
45
38
  }
46
- //# sourceMappingURL=guest-key.js.map
@@ -2,9 +2,8 @@ import { type MachineKeypair } from "@getmyenv/crypto";
2
2
  import { type MachineProof } from "@getmyenv/shared";
3
3
  /**
4
4
  * Machine identity per project, stored in the OS user config (never the repo).
5
- * It identifies this machine for locked contexts. It does not decrypt values.
5
+ * It identifies this machine for read-only contexts. It does not decrypt values.
6
6
  */
7
7
  export declare function loadOrCreateIdentity(projectId: string): MachineKeypair;
8
8
  export declare function machineLabel(): string;
9
9
  export declare function signMachineProof(projectId: string, contextSlug: string): MachineProof;
10
- //# sourceMappingURL=identity.d.ts.map
@@ -6,7 +6,7 @@ import { machineProofMessage } from "@getmyenv/shared";
6
6
  import { getConfigDir } from "./fs.js";
7
7
  /**
8
8
  * Machine identity per project, stored in the OS user config (never the repo).
9
- * It identifies this machine for locked contexts. It does not decrypt values.
9
+ * It identifies this machine for read-only contexts. It does not decrypt values.
10
10
  */
11
11
  export function loadOrCreateIdentity(projectId) {
12
12
  const dir = path.join(getConfigDir(), "machines");
@@ -45,4 +45,3 @@ export function signMachineProof(projectId, contextSlug) {
45
45
  timestamp,
46
46
  };
47
47
  }
48
- //# sourceMappingURL=identity.js.map
@@ -5,17 +5,30 @@ type KeyringEntry = {
5
5
  deletePassword(): boolean;
6
6
  };
7
7
  export type KeyringFactory = (service: string, account: string) => KeyringEntry;
8
+ export type KeyringAccounts = (service: string) => string[];
8
9
  /** Tests swap in an in-memory keyring. `null` means no keyring available. */
9
- export declare function setKeyringForTests(factory: KeyringFactory | null | undefined): void;
10
+ export declare function setKeyringForTests(factory: KeyringFactory | null | undefined, accounts?: KeyringAccounts): void;
10
11
  /** Off with `GETMYENV_KEYCHAIN=0` or in CI (`CI` set to anything but `false` or `0`). */
11
12
  export declare function keychainEnabled(env?: NodeJS.ProcessEnv): boolean;
12
13
  /** One keychain entry per server origin and project, so staging and production keys never mix. */
13
14
  export declare function keychainAccount(apiUrl: string, projectId: string): string;
14
- /** The cached key, only if it has not expired and matches the server's wrapped key. */
15
- export declare function readCachedPek(account: string, wrappedRaw: string, now?: number): Promise<Uint8Array | null>;
15
+ /**
16
+ * The cached key of one context, only if it has not expired and matches the
17
+ * sealed key the server holds now. A new sealed key (rotation) misses.
18
+ */
19
+ export declare function readCachedContextKey(account: string, contextId: number, sealedRaw: string, now?: number): Promise<Uint8Array | null>;
16
20
  /** Returns false when no keychain is available; the CLI then keeps asking. */
17
- export declare function storeCachedPek(account: string, wrappedRaw: string, pek: Uint8Array, now?: number): Promise<boolean>;
21
+ export declare function storeCachedContextKeys(account: string, entries: Array<{
22
+ contextId: number;
23
+ sealedRaw: string;
24
+ key: Uint8Array;
25
+ }>, now?: number): Promise<boolean>;
18
26
  /** True when an entry was removed. False when there was none or no keychain is available. */
19
- export declare function clearCachedPek(account: string): Promise<boolean>;
27
+ export declare function clearCachedKeys(account: string): Promise<boolean>;
28
+ /**
29
+ * Removes every getmyenv entry on this machine, for all servers and projects.
30
+ * `fallback` accounts are cleared too, for keychains that cannot list entries.
31
+ * Returns how many entries were removed.
32
+ */
33
+ export declare function clearAllCachedKeys(fallback?: string[]): Promise<number>;
20
34
  export {};
21
- //# sourceMappingURL=keychain.d.ts.map
@@ -1,16 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  /**
3
- * The unlocked project key, remembered in the OS keychain (macOS Keychain,
3
+ * Unlocked open context keys, remembered in the OS keychain (macOS Keychain,
4
4
  * Windows Credential Manager, Linux Secret Service) for a few hours so `run`
5
5
  * on an open context does not ask for the Vault password every time.
6
- * Never the Vault password itself.
6
+ * Never the Vault password or the private key.
7
7
  */
8
8
  const SERVICE = "getmyenv";
9
9
  export const KEYCHAIN_TTL_MS = 8 * 60 * 60 * 1000;
10
10
  let factoryOverride;
11
+ let accountsOverride;
11
12
  /** Tests swap in an in-memory keyring. `null` means no keyring available. */
12
- export function setKeyringForTests(factory) {
13
+ export function setKeyringForTests(factory, accounts) {
13
14
  factoryOverride = factory;
15
+ accountsOverride = accounts;
14
16
  }
15
17
  async function loadFactory() {
16
18
  if (factoryOverride !== undefined)
@@ -23,6 +25,17 @@ async function loadFactory() {
23
25
  return null;
24
26
  }
25
27
  }
28
+ async function loadAccounts() {
29
+ if (factoryOverride !== undefined)
30
+ return accountsOverride ?? null;
31
+ try {
32
+ const { findCredentials } = await import("@napi-rs/keyring");
33
+ return (service) => findCredentials(service).map((c) => c.account);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
26
39
  /** Off with `GETMYENV_KEYCHAIN=0` or in CI (`CI` set to anything but `false` or `0`). */
27
40
  export function keychainEnabled(env = process.env) {
28
41
  if (env.GETMYENV_KEYCHAIN === "0")
@@ -43,8 +56,8 @@ export function keychainAccount(apiUrl, projectId) {
43
56
  }
44
57
  return `${origin}|project:${projectId}`;
45
58
  }
46
- function hashWrapped(wrappedRaw) {
47
- return createHash("sha256").update(wrappedRaw).digest("hex");
59
+ function hashSealed(sealedRaw) {
60
+ return createHash("sha256").update(sealedRaw).digest("hex");
48
61
  }
49
62
  async function entryFor(account) {
50
63
  const factory = await loadFactory();
@@ -57,11 +70,7 @@ async function entryFor(account) {
57
70
  return null;
58
71
  }
59
72
  }
60
- /** The cached key, only if it has not expired and matches the server's wrapped key. */
61
- export async function readCachedPek(account, wrappedRaw, now = Date.now()) {
62
- const entry = await entryFor(account);
63
- if (!entry)
64
- return null;
73
+ function readStored(entry, now) {
65
74
  let raw;
66
75
  try {
67
76
  raw = entry.getPassword();
@@ -79,24 +88,43 @@ export async function readCachedPek(account, wrappedRaw, now = Date.now()) {
79
88
  safeDelete(entry);
80
89
  return null;
81
90
  }
82
- if (typeof stored.pek !== "string" ||
83
- stored.wrappedHash !== hashWrapped(wrappedRaw) ||
91
+ if (stored.version !== 2 ||
84
92
  typeof stored.expiresAt !== "number" ||
85
- stored.expiresAt <= now) {
93
+ stored.expiresAt <= now ||
94
+ !stored.keys ||
95
+ typeof stored.keys !== "object") {
86
96
  safeDelete(entry);
87
97
  return null;
88
98
  }
89
- return new Uint8Array(Buffer.from(stored.pek, "base64"));
99
+ return stored;
100
+ }
101
+ /**
102
+ * The cached key of one context, only if it has not expired and matches the
103
+ * sealed key the server holds now. A new sealed key (rotation) misses.
104
+ */
105
+ export async function readCachedContextKey(account, contextId, sealedRaw, now = Date.now()) {
106
+ const entry = await entryFor(account);
107
+ if (!entry)
108
+ return null;
109
+ const stored = readStored(entry, now);
110
+ const hit = stored?.keys[String(contextId)];
111
+ if (!hit || typeof hit.key !== "string" || hit.sealedHash !== hashSealed(sealedRaw))
112
+ return null;
113
+ const key = new Uint8Array(Buffer.from(hit.key, "base64"));
114
+ return key.length === 32 ? key : null;
90
115
  }
91
116
  /** Returns false when no keychain is available; the CLI then keeps asking. */
92
- export async function storeCachedPek(account, wrappedRaw, pek, now = Date.now()) {
117
+ export async function storeCachedContextKeys(account, entries, now = Date.now()) {
93
118
  const entry = await entryFor(account);
94
119
  if (!entry)
95
120
  return false;
96
121
  const stored = {
97
- pek: Buffer.from(pek).toString("base64"),
98
- wrappedHash: hashWrapped(wrappedRaw),
122
+ version: 2,
99
123
  expiresAt: now + KEYCHAIN_TTL_MS,
124
+ keys: Object.fromEntries(entries.map((e) => [
125
+ String(e.contextId),
126
+ { key: Buffer.from(e.key).toString("base64"), sealedHash: hashSealed(e.sealedRaw) },
127
+ ])),
100
128
  };
101
129
  try {
102
130
  entry.setPassword(JSON.stringify(stored));
@@ -107,12 +135,35 @@ export async function storeCachedPek(account, wrappedRaw, pek, now = Date.now())
107
135
  }
108
136
  }
109
137
  /** True when an entry was removed. False when there was none or no keychain is available. */
110
- export async function clearCachedPek(account) {
138
+ export async function clearCachedKeys(account) {
111
139
  const entry = await entryFor(account);
112
140
  if (!entry)
113
141
  return false;
114
142
  return safeDelete(entry);
115
143
  }
144
+ /**
145
+ * Removes every getmyenv entry on this machine, for all servers and projects.
146
+ * `fallback` accounts are cleared too, for keychains that cannot list entries.
147
+ * Returns how many entries were removed.
148
+ */
149
+ export async function clearAllCachedKeys(fallback = []) {
150
+ let accounts = [];
151
+ const list = await loadAccounts();
152
+ if (list) {
153
+ try {
154
+ accounts = list(SERVICE);
155
+ }
156
+ catch {
157
+ accounts = [];
158
+ }
159
+ }
160
+ let removed = 0;
161
+ for (const account of new Set([...accounts, ...fallback])) {
162
+ if (await clearCachedKeys(account))
163
+ removed++;
164
+ }
165
+ return removed;
166
+ }
116
167
  function safeDelete(entry) {
117
168
  try {
118
169
  return entry.deletePassword();
@@ -121,4 +172,3 @@ function safeDelete(entry) {
121
172
  return false;
122
173
  }
123
174
  }
124
- //# sourceMappingURL=keychain.js.map
@@ -0,0 +1,3 @@
1
+ /** "Did you mean OPENAI_API_KEY?" for names close to a known one. Nothing is renamed. */
2
+ export declare function didYouMean(name: string): string | null;
3
+ export declare function printDidYouMean(names: string[], indent?: string): void;
@@ -0,0 +1,17 @@
1
+ import pc from "picocolors";
2
+ import { nearMissKnownKey } from "@getmyenv/shared";
3
+ /** "Did you mean OPENAI_API_KEY?" for names close to a known one. Nothing is renamed. */
4
+ export function didYouMean(name) {
5
+ const near = nearMissKnownKey(name);
6
+ if (!near)
7
+ return null;
8
+ const why = near.source === "sdk" ? `${near.provider} SDKs read that name.` : "That is the common name.";
9
+ return `Did you mean ${near.name}? ${why}`;
10
+ }
11
+ export function printDidYouMean(names, indent = "") {
12
+ for (const name of names) {
13
+ const hint = didYouMean(name);
14
+ if (hint)
15
+ console.error(pc.yellow(`${indent}${name}: ${hint}`));
16
+ }
17
+ }
@@ -0,0 +1,25 @@
1
+ import type { SealedKey } from "@getmyenv/crypto";
2
+ import { ApiClient, type Protection } from "./api.js";
3
+ import type { MissingKey } from "./run-env.js";
4
+ export type ResolveOk = {
5
+ status: "ok";
6
+ /** How the read was allowed. */
7
+ via: "open" | "trusted" | "auto" | "grant";
8
+ context: {
9
+ id: number;
10
+ slug: string;
11
+ name: string;
12
+ protection: Protection;
13
+ };
14
+ sealedKey: SealedKey | null;
15
+ values: Array<{
16
+ name: string;
17
+ ciphertext: string;
18
+ }>;
19
+ missing: MissingKey[];
20
+ };
21
+ /**
22
+ * Ciphertext for one context. Read-only contexts need a server token, an
23
+ * allowed IP, and a trusted machine or an approval in the dashboard.
24
+ */
25
+ export declare function resolveValues(client: ApiClient, projectId: string, slug: string, purpose: "run" | "export"): Promise<ResolveOk>;
@@ -0,0 +1,56 @@
1
+ import pc from "picocolors";
2
+ import { ApiError } from "./api.js";
3
+ import { sleep } from "./authorize.js";
4
+ import { signMachineProof } from "./identity.js";
5
+ const GRANT_HEADER = "X-Getmyenv-Grant";
6
+ /** Status lines go to stderr so stdout stays clean for pipes. */
7
+ const log = (line = "") => console.error(line);
8
+ async function resolveOnce(client, projectId, slug, purpose, grant) {
9
+ return client.request("POST", "/api/cli/resolve", { context: slug, machine: signMachineProof(projectId, slug), purpose }, grant ? { [GRANT_HEADER]: grant } : undefined);
10
+ }
11
+ async function waitForGrant(client, pending) {
12
+ log(`Waiting for approval in the dashboard: ${pending.reviewUrl}`);
13
+ log(pc.dim("Trust this machine there to skip approval next time.\n"));
14
+ const deadline = new Date(pending.expiresAt).getTime();
15
+ while (Date.now() < deadline) {
16
+ await sleep(1500);
17
+ const s = await client.request("GET", `/api/cli/requests/${pending.requestId}`);
18
+ if (s.status === "approved" && s.grantToken) {
19
+ log(`${pc.green("✓")} Approved\n`);
20
+ return s.grantToken;
21
+ }
22
+ if (s.status === "approved") {
23
+ throw new Error("This approval was already used. Run the command again.");
24
+ }
25
+ if (s.status === "denied")
26
+ throw new Error("Access denied in the dashboard.");
27
+ if (s.status === "expired")
28
+ break;
29
+ }
30
+ throw new Error("Approval request expired. Run the command again.");
31
+ }
32
+ /**
33
+ * Ciphertext for one context. Read-only contexts need a server token, an
34
+ * allowed IP, and a trusted machine or an approval in the dashboard.
35
+ */
36
+ export async function resolveValues(client, projectId, slug, purpose) {
37
+ let res = await resolveOnce(client, projectId, slug, purpose);
38
+ if (res.status === "pending") {
39
+ const grant = await waitForGrant(client, res);
40
+ try {
41
+ res = await resolveOnce(client, projectId, slug, purpose, grant);
42
+ }
43
+ catch (err) {
44
+ if (err instanceof ApiError && err.status === 401) {
45
+ throw new Error(`${err.message}. Run the command again.`);
46
+ }
47
+ throw err;
48
+ }
49
+ if (res.status !== "ok")
50
+ throw new Error("Approval did not complete. Run the command again.");
51
+ }
52
+ if (res.via === "auto") {
53
+ log(`${pc.green("✓")} This machine is now trusted for ${res.context.slug}.`);
54
+ }
55
+ return res;
56
+ }
@@ -1,6 +1,5 @@
1
- /**
2
- * Pure pieces of `getmyenv run`: validate and merge. Names only in any output.
3
- */
1
+ /** Names from the server go into an environment or a file. Refuse any that are not variable names. */
2
+ export declare function assertVariableNames(names: Iterable<string>): void;
4
3
  export type MissingKey = {
5
4
  name: string;
6
5
  reason: "missing" | "expired";
@@ -47,4 +46,3 @@ export declare function windowsCommandLine(args: string[]): string;
47
46
  export declare function signalExitCode(signal: NodeJS.Signals, numbers: Record<string, number>): number;
48
47
  /** `--explain` output. Names and origins only, never values. */
49
48
  export declare function explainLines(result: MergeResult, contextSlug: string): string[];
50
- //# sourceMappingURL=run-env.d.ts.map
@@ -1,6 +1,15 @@
1
1
  /**
2
2
  * Pure pieces of `getmyenv run`: validate and merge. Names only in any output.
3
3
  */
4
+ import { isValidVariableName } from "@getmyenv/shared";
5
+ /** Names from the server go into an environment or a file. Refuse any that are not variable names. */
6
+ export function assertVariableNames(names) {
7
+ for (const name of names) {
8
+ if (!isValidVariableName(name)) {
9
+ throw new Error("The server sent a name that is not a valid variable name. Nothing was run or written.");
10
+ }
11
+ }
12
+ }
4
13
  /** The child never sees the CLI's own settings (token, API URL, config dir). */
5
14
  export function withoutOwnEnv(parent) {
6
15
  const env = {};
@@ -111,4 +120,3 @@ export function explainLines(result, contextSlug) {
111
120
  lines.push(` ${result.applied.length} from getmyenv, ${result.overridden.length} replaced, ${result.kept.length} kept, ${result.filledByParent.length} from parent env, ${result.unresolved.length} not set`);
112
121
  return lines;
113
122
  }
114
- //# sourceMappingURL=run-env.js.map
@@ -11,4 +11,3 @@ export declare function updateLine(current: string, latest: string | null): stri
11
11
  * cached npm version, refreshed in the background once a day. Skipped in CI.
12
12
  */
13
13
  export declare function maybeSuggestUpdate(): Promise<void>;
14
- //# sourceMappingURL=update-check.d.ts.map
@@ -90,7 +90,10 @@ async function fetchAndCache(file, timeoutMs) {
90
90
  const ctrl = new AbortController();
91
91
  setTimeout(() => ctrl.abort(), timeoutMs).unref();
92
92
  try {
93
- const res = await fetch("https://registry.npmjs.org/getmyenv/latest", { signal: ctrl.signal });
93
+ const res = await fetch("https://registry.npmjs.org/getmyenv/latest", {
94
+ signal: ctrl.signal,
95
+ redirect: "error",
96
+ });
94
97
  if (!res.ok)
95
98
  return;
96
99
  const data = (await res.json());
@@ -132,4 +135,3 @@ export async function maybeSuggestUpdate() {
132
135
  if (line)
133
136
  console.error(pc.yellow(line));
134
137
  }
135
- //# sourceMappingURL=update-check.js.map
@@ -12,10 +12,8 @@ export declare function uploadValues(input: {
12
12
  pairs: Record<string, string>;
13
13
  prune?: boolean;
14
14
  expiresAt?: string | null;
15
- vaultPasswordStdin?: boolean;
16
15
  }): Promise<{
17
16
  set: string[];
18
17
  pruned: string[];
19
18
  declared: string[];
20
19
  }>;
21
- //# sourceMappingURL=upload.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { encryptSecret, serializePayload } from "@getmyenv/crypto";
2
- import { unlockProjectKey } from "./unlock.js";
2
+ import { contextKey } from "./vault.js";
3
3
  /** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
4
4
  export function assertWritable(linked, ctx) {
5
5
  if (linked.who.token.kind === "server") {
@@ -21,33 +21,24 @@ export async function uploadValues(input) {
21
21
  .filter(([, v]) => v === "")
22
22
  .map(([k]) => k);
23
23
  let values = [];
24
- let wrappedProjectKey;
25
24
  if (filled.length > 0) {
26
- const { wrappedProjectKey: serverWrapped } = await linked.client.request("GET", "/api/cli/vault");
27
- const { pek, wrappedRaw, created } = await unlockProjectKey({
28
- isGuest: linked.who.isGuest,
29
- projectId: linked.who.project.id,
30
- wrapped: serverWrapped,
31
- cache: true,
32
- allowCreate: true,
33
- vaultPasswordStdin: input.vaultPasswordStdin,
34
- apiUrl: linked.apiUrl,
35
- });
36
- if (serverWrapped == null || created) {
37
- wrappedProjectKey = JSON.parse(wrappedRaw);
38
- }
25
+ const key = await contextKey(linked, context, { create: true });
39
26
  values = filled.map(([name, value]) => ({
40
27
  name,
41
- ciphertext: serializePayload(encryptSecret(pek, value)),
28
+ ciphertext: serializePayload(encryptSecret(key, name, value)),
42
29
  ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
43
30
  }));
44
31
  }
45
- return linked.client.request("POST", "/api/cli/values", {
32
+ const body = {
46
33
  context: context.slug,
47
- ...(wrappedProjectKey ? { wrappedProjectKey } : {}),
48
34
  values,
49
35
  ...(input.prune ? { prune: true } : {}),
50
36
  ...(declare.length > 0 ? { declare } : {}),
51
- });
37
+ };
38
+ if (Buffer.byteLength(JSON.stringify(body)) > UPLOAD_MAX_BYTES) {
39
+ throw new Error("Too much to upload at once (max 1 MB encrypted). Split the file and import each part.");
40
+ }
41
+ return linked.client.request("POST", "/api/cli/values", body);
52
42
  }
53
- //# sourceMappingURL=upload.js.map
43
+ /** Matches the server limit on `/api/cli/values`. */
44
+ const UPLOAD_MAX_BYTES = 1024 * 1024;
@@ -0,0 +1,54 @@
1
+ import { type SealedKey, type WrappedPrivateKey } from "@getmyenv/crypto";
2
+ import { type Protection } from "./api.js";
3
+ import type { Linked } from "./bootstrap.js";
4
+ /**
5
+ * Context keys for the CLI.
6
+ *
7
+ * Folder and guest tokens: the account (or guest) private key is wrapped with
8
+ * the Vault password (or the guest passphrase in claim.json). It is unwrapped
9
+ * here, opens the sealed context keys, and is wiped. Open context keys may
10
+ * stay in the OS keychain for 8 hours. The private key is never stored.
11
+ *
12
+ * Server tokens: GETMYENV_TOKEN holds the token's own private key. It opens
13
+ * the one context key sealed to that token. No Vault password.
14
+ */
15
+ export type VaultContext = {
16
+ id: number;
17
+ slug: string;
18
+ protection: Protection;
19
+ hasKey: boolean;
20
+ sealedKey: SealedKey | null;
21
+ };
22
+ export type VaultInfo = {
23
+ holder: "user" | "guest" | "token";
24
+ publicKey: string | null;
25
+ wrappedPrivateKey: WrappedPrivateKey | null;
26
+ contexts: VaultContext[];
27
+ claim: {
28
+ publicKey: string;
29
+ wrappedPrivateKey: WrappedPrivateKey;
30
+ shares: Array<{
31
+ contextId: number;
32
+ sealedKey: SealedKey;
33
+ }>;
34
+ } | null;
35
+ };
36
+ export type KeyTarget = {
37
+ id: number;
38
+ slug: string;
39
+ protection: Protection;
40
+ /** Sealed key already sent by the server, so no extra request is needed. */
41
+ sealedKey?: SealedKey | null;
42
+ };
43
+ export type KeyOpts = {
44
+ /** Make the Vault password and the context key when they do not exist yet. */
45
+ create?: boolean;
46
+ };
47
+ export declare function fetchVault(linked: Linked): Promise<VaultInfo>;
48
+ /** The context key, unlocking with the Vault password when needed. */
49
+ export declare function contextKey(linked: Linked, target: KeyTarget, opts?: KeyOpts): Promise<Uint8Array>;
50
+ /** Unlock with the Vault password and return the private key. Caller wipes it. */
51
+ export declare function unlockPrivateKey(linked: Linked, opts?: KeyOpts): Promise<{
52
+ vault: VaultInfo;
53
+ privateKey: Uint8Array;
54
+ }>;