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.
- package/README.md +19 -17
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +2 -2
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +13 -17
- package/dist/commands/backup.d.ts +1 -2
- package/dist/commands/backup.js +11 -7
- package/dist/commands/claim.d.ts +1 -2
- package/dist/commands/claim.js +84 -40
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +23 -7
- package/dist/commands/export.d.ts +5 -3
- package/dist/commands/export.js +21 -20
- package/dist/commands/guest.d.ts +2 -3
- package/dist/commands/guest.js +10 -11
- package/dist/commands/import.d.ts +2 -2
- package/dist/commands/import.js +6 -4
- package/dist/commands/lock.d.ts +1 -2
- package/dist/commands/lock.js +5 -9
- package/dist/commands/ls.d.ts +0 -1
- package/dist/commands/ls.js +0 -1
- package/dist/commands/restore.d.ts +3 -4
- package/dist/commands/restore.js +52 -28
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +9 -62
- package/dist/commands/set.d.ts +0 -2
- package/dist/commands/set.js +4 -17
- package/dist/commands/start.d.ts +8 -3
- package/dist/commands/start.js +49 -11
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/status.js +3 -2
- package/dist/commands/template.d.ts +13 -0
- package/dist/commands/template.js +75 -0
- package/dist/commands/unlink.d.ts +0 -1
- package/dist/commands/unlink.js +5 -9
- package/dist/lib/api-url.d.ts +0 -1
- package/dist/lib/api-url.js +0 -1
- package/dist/lib/api.d.ts +13 -8
- package/dist/lib/api.js +10 -5
- package/dist/lib/authorize.d.ts +0 -1
- package/dist/lib/authorize.js +4 -5
- package/dist/lib/bootstrap.d.ts +0 -1
- package/dist/lib/bootstrap.js +6 -4
- package/dist/lib/confirm.d.ts +0 -1
- package/dist/lib/confirm.js +0 -1
- package/dist/lib/context.d.ts +3 -2
- package/dist/lib/context.js +4 -2
- package/dist/lib/credentials.d.ts +4 -1
- package/dist/lib/credentials.js +28 -2
- package/dist/lib/fs.d.ts +10 -7
- package/dist/lib/fs.js +37 -22
- package/dist/lib/guest-key.d.ts +7 -4
- package/dist/lib/guest-key.js +22 -30
- package/dist/lib/identity.d.ts +1 -2
- package/dist/lib/identity.js +1 -2
- package/dist/lib/keychain.d.ts +19 -6
- package/dist/lib/keychain.js +69 -19
- package/dist/lib/known-keys.d.ts +3 -0
- package/dist/lib/known-keys.js +17 -0
- package/dist/lib/resolve.d.ts +25 -0
- package/dist/lib/resolve.js +56 -0
- package/dist/lib/run-env.d.ts +2 -4
- package/dist/lib/run-env.js +9 -1
- package/dist/lib/update-check.d.ts +0 -1
- package/dist/lib/update-check.js +4 -2
- package/dist/lib/upload.d.ts +0 -2
- package/dist/lib/upload.js +11 -20
- package/dist/lib/vault.d.ts +54 -0
- package/dist/lib/vault.js +174 -0
- package/package.json +6 -7
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/backup.d.ts.map +0 -1
- package/dist/commands/backup.js.map +0 -1
- package/dist/commands/claim.d.ts.map +0 -1
- package/dist/commands/claim.js.map +0 -1
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/export.d.ts.map +0 -1
- package/dist/commands/export.js.map +0 -1
- package/dist/commands/guest.d.ts.map +0 -1
- package/dist/commands/guest.js.map +0 -1
- package/dist/commands/import.d.ts.map +0 -1
- package/dist/commands/import.js.map +0 -1
- package/dist/commands/lock.d.ts.map +0 -1
- package/dist/commands/lock.js.map +0 -1
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js.map +0 -1
- package/dist/commands/restore.d.ts.map +0 -1
- package/dist/commands/restore.js.map +0 -1
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/set.d.ts.map +0 -1
- package/dist/commands/set.js.map +0 -1
- package/dist/commands/start.d.ts.map +0 -1
- package/dist/commands/start.js.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/unlink.d.ts.map +0 -1
- package/dist/commands/unlink.js.map +0 -1
- package/dist/lib/api-url.d.ts.map +0 -1
- package/dist/lib/api-url.js.map +0 -1
- package/dist/lib/api.d.ts.map +0 -1
- package/dist/lib/api.js.map +0 -1
- package/dist/lib/authorize.d.ts.map +0 -1
- package/dist/lib/authorize.js.map +0 -1
- package/dist/lib/bootstrap.d.ts.map +0 -1
- package/dist/lib/bootstrap.js.map +0 -1
- package/dist/lib/confirm.d.ts.map +0 -1
- package/dist/lib/confirm.js.map +0 -1
- package/dist/lib/context.d.ts.map +0 -1
- package/dist/lib/context.js.map +0 -1
- package/dist/lib/credentials.d.ts.map +0 -1
- package/dist/lib/credentials.js.map +0 -1
- package/dist/lib/fs.d.ts.map +0 -1
- package/dist/lib/fs.js.map +0 -1
- package/dist/lib/guest-key.d.ts.map +0 -1
- package/dist/lib/guest-key.js.map +0 -1
- package/dist/lib/identity.d.ts.map +0 -1
- package/dist/lib/identity.js.map +0 -1
- package/dist/lib/keychain.d.ts.map +0 -1
- package/dist/lib/keychain.js.map +0 -1
- package/dist/lib/pek.d.ts +0 -27
- package/dist/lib/pek.d.ts.map +0 -1
- package/dist/lib/pek.js +0 -60
- package/dist/lib/pek.js.map +0 -1
- package/dist/lib/run-env.d.ts.map +0 -1
- package/dist/lib/run-env.js.map +0 -1
- package/dist/lib/unlock.d.ts +0 -24
- package/dist/lib/unlock.d.ts.map +0 -1
- package/dist/lib/unlock.js +0 -83
- package/dist/lib/unlock.js.map +0 -1
- package/dist/lib/update-check.d.ts.map +0 -1
- package/dist/lib/update-check.js.map +0 -1
- package/dist/lib/upload.d.ts.map +0 -1
- package/dist/lib/upload.js.map +0 -1
package/dist/lib/guest-key.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
|
|
39
|
-
|
|
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
|
package/dist/lib/identity.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/lib/identity.js
CHANGED
|
@@ -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
|
|
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
|
package/dist/lib/keychain.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
15
|
-
|
|
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
|
|
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
|
|
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
|
package/dist/lib/keychain.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
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
|
|
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
|
|
47
|
-
return createHash("sha256").update(
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,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
|
+
}
|
package/dist/lib/run-env.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
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
|
package/dist/lib/run-env.js
CHANGED
|
@@ -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
|
package/dist/lib/update-check.js
CHANGED
|
@@ -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", {
|
|
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
|
package/dist/lib/upload.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/upload.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encryptSecret, serializePayload } from "@getmyenv/crypto";
|
|
2
|
-
import {
|
|
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
|
|
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(
|
|
28
|
+
ciphertext: serializePayload(encryptSecret(key, name, value)),
|
|
42
29
|
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
43
30
|
}));
|
|
44
31
|
}
|
|
45
|
-
|
|
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
|
-
|
|
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
|
+
}>;
|