getmyenv 0.9.7 → 0.9.9
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 +1 -1
- package/dist/commands/guest.d.ts +2 -0
- package/dist/commands/guest.js +26 -5
- package/dist/commands/restore.js +5 -2
- package/dist/lib/upload.js +6 -3
- package/dist/lib/vault.d.ts +11 -0
- package/dist/lib/vault.js +23 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Prompts and status lines go to stderr, so `export --stdout > .env` stays clean.
|
|
|
35
35
|
Commit `.getmyenv/project.json`. `token.json` and `claim.json` stay local and are gitignored.
|
|
36
36
|
|
|
37
37
|
<!-- cli:help -->
|
|
38
|
-
Output of `npx getmyenv --help` (0.9.
|
|
38
|
+
Output of `npx getmyenv --help` (0.9.9):
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
41
|
Usage: getmyenv [options] [command]
|
package/dist/commands/guest.d.ts
CHANGED
|
@@ -13,4 +13,6 @@ export declare function createGuestProject(opts?: GuestOpts): Promise<{
|
|
|
13
13
|
projectName: string;
|
|
14
14
|
guestExpiresAt: string;
|
|
15
15
|
}>;
|
|
16
|
+
/** The steps printed after guest, in plain text. Runtime first, export only as an alternative. */
|
|
17
|
+
export declare function guestNextLines(): string[];
|
|
16
18
|
export declare function guestCommand(opts?: GuestOpts): Promise<void>;
|
package/dist/commands/guest.js
CHANGED
|
@@ -61,13 +61,34 @@ export async function createGuestProject(opts = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
return created;
|
|
63
63
|
}
|
|
64
|
+
/** The steps printed after guest, in plain text. Runtime first, export only as an alternative. */
|
|
65
|
+
export function guestNextLines() {
|
|
66
|
+
return [
|
|
67
|
+
"Next:",
|
|
68
|
+
" npx getmyenv import # from .env",
|
|
69
|
+
" npx getmyenv set API_KEY # or one at a time",
|
|
70
|
+
" npx getmyenv run -- npm start # your start command, no .env file on disk",
|
|
71
|
+
" npx getmyenv claim # keep it, with an account",
|
|
72
|
+
"",
|
|
73
|
+
"Only if a tool needs a .env file:",
|
|
74
|
+
" npx getmyenv export # writes a plaintext .env",
|
|
75
|
+
"",
|
|
76
|
+
`For AI agents: ${FACTS.agentRunOrExport}`,
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
function colorNextLine(line) {
|
|
80
|
+
const hash = line.indexOf("# ");
|
|
81
|
+
if (line.startsWith(" npx ") && hash > 0) {
|
|
82
|
+
return pc.cyan(line.slice(0, hash)) + pc.dim(line.slice(hash));
|
|
83
|
+
}
|
|
84
|
+
if (line.startsWith("For AI agents:"))
|
|
85
|
+
return pc.dim(line);
|
|
86
|
+
return line;
|
|
87
|
+
}
|
|
64
88
|
export async function guestCommand(opts = {}) {
|
|
65
89
|
console.error(pc.bold("\ngetmyenv guest\n"));
|
|
66
90
|
await createGuestProject(opts);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.error(pc.cyan(" npx getmyenv set API_KEY ") + pc.dim("# or one at a time"));
|
|
70
|
-
console.error(pc.cyan(" npx getmyenv run -- npm start ") + pc.dim("# your start command"));
|
|
71
|
-
console.error(pc.cyan(" npx getmyenv claim ") + pc.dim("# keep it, with an account"));
|
|
91
|
+
for (const line of guestNextLines())
|
|
92
|
+
console.error(colorNextLine(line));
|
|
72
93
|
console.error("");
|
|
73
94
|
}
|
package/dist/commands/restore.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
|
5
5
|
import { findContext } from "../lib/context.js";
|
|
6
6
|
import { promptConfirm } from "../lib/fs.js";
|
|
7
7
|
import { resolveVaultPassword } from "../lib/guest-key.js";
|
|
8
|
-
import {
|
|
8
|
+
import { writeKey } from "../lib/vault.js";
|
|
9
9
|
/**
|
|
10
10
|
* Restore a backup into this folder's project. The backup opens locally with
|
|
11
11
|
* the Vault password it was made with. Values are encrypted again with this
|
|
@@ -73,11 +73,13 @@ export async function restoreCommand(file, opts) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
const values = [];
|
|
76
|
+
const keyVersions = {};
|
|
76
77
|
for (const v of opened.values) {
|
|
77
78
|
if (!writable(v.context))
|
|
78
79
|
continue;
|
|
79
80
|
const target = targets.get(v.context);
|
|
80
|
-
const key = await
|
|
81
|
+
const { key, keyVersion } = await writeKey(linked, target);
|
|
82
|
+
keyVersions[v.context] = keyVersion;
|
|
81
83
|
values.push({
|
|
82
84
|
key: v.key,
|
|
83
85
|
context: v.context,
|
|
@@ -89,6 +91,7 @@ export async function restoreCommand(file, opts) {
|
|
|
89
91
|
keys: envelope.keys,
|
|
90
92
|
tags: envelope.tags ?? {},
|
|
91
93
|
values,
|
|
94
|
+
keyVersions,
|
|
92
95
|
});
|
|
93
96
|
const skipped = [...perContext.keys()].filter((slug) => !writable(slug));
|
|
94
97
|
console.log(`${pc.green("✓")} Restored ${res.restored} values into ${res.projectName}`);
|
package/dist/lib/upload.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { encryptSecret, serializePayload } from "@getmyenv/crypto";
|
|
2
2
|
import { FACTS } from "@getmyenv/shared";
|
|
3
3
|
import { ApiError } from "./api.js";
|
|
4
|
-
import {
|
|
4
|
+
import { writeKey } from "./vault.js";
|
|
5
5
|
/** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
|
|
6
6
|
export function assertWritable(linked, ctx) {
|
|
7
7
|
if (linked.who.token.kind === "server") {
|
|
@@ -23,17 +23,20 @@ export async function uploadValues(input) {
|
|
|
23
23
|
.filter(([, v]) => v === "")
|
|
24
24
|
.map(([k]) => k);
|
|
25
25
|
let values = [];
|
|
26
|
+
let keyVersion;
|
|
26
27
|
if (filled.length > 0) {
|
|
27
|
-
const
|
|
28
|
+
const write = await writeKey(linked, context);
|
|
29
|
+
keyVersion = write.keyVersion;
|
|
28
30
|
values = filled.map(([name, value]) => ({
|
|
29
31
|
name,
|
|
30
|
-
ciphertext: serializePayload(encryptSecret(key, name, value)),
|
|
32
|
+
ciphertext: serializePayload(encryptSecret(write.key, name, value)),
|
|
31
33
|
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
32
34
|
}));
|
|
33
35
|
}
|
|
34
36
|
const body = {
|
|
35
37
|
context: context.slug,
|
|
36
38
|
values,
|
|
39
|
+
...(keyVersion !== undefined ? { keyVersion } : {}),
|
|
37
40
|
...(input.prune ? { prune: true } : {}),
|
|
38
41
|
...(declare.length > 0 ? { declare } : {}),
|
|
39
42
|
};
|
package/dist/lib/vault.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type VaultContext = {
|
|
|
18
18
|
protection: Protection;
|
|
19
19
|
hasKey: boolean;
|
|
20
20
|
sealedKey: SealedKey | null;
|
|
21
|
+
/** Version of the key in sealedKey. Sent back with every write. */
|
|
22
|
+
keyVersion: number;
|
|
21
23
|
};
|
|
22
24
|
export type VaultInfo = {
|
|
23
25
|
holder: "user" | "guest" | "token";
|
|
@@ -45,6 +47,15 @@ export type KeyOpts = {
|
|
|
45
47
|
create?: boolean;
|
|
46
48
|
};
|
|
47
49
|
export declare function fetchVault(linked: Linked): Promise<VaultInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* The context key and its version, for a write. The server refuses ciphertext
|
|
52
|
+
* written with a key that was replaced since, so the version comes from the
|
|
53
|
+
* same vault response as the sealed key.
|
|
54
|
+
*/
|
|
55
|
+
export declare function writeKey(linked: Linked, target: KeyTarget): Promise<{
|
|
56
|
+
key: Uint8Array;
|
|
57
|
+
keyVersion: number;
|
|
58
|
+
}>;
|
|
48
59
|
/** The context key, unlocking with the Vault password when needed. */
|
|
49
60
|
export declare function contextKey(linked: Linked, target: KeyTarget, opts?: KeyOpts): Promise<Uint8Array>;
|
|
50
61
|
/** Unlock with the Vault password and return the private key. Caller wipes it. */
|
package/dist/lib/vault.js
CHANGED
|
@@ -6,9 +6,25 @@ import { resolveVaultPassword } from "./guest-key.js";
|
|
|
6
6
|
import { keychainAccount, keychainEnabled, readCachedContextKey, storeCachedContextKeys, } from "./keychain.js";
|
|
7
7
|
/** One unlock per process: several contexts in one command prompt once. */
|
|
8
8
|
const memo = new Map();
|
|
9
|
+
/** Key version of each memo entry opened from the vault response. Keys from the keychain have none. */
|
|
10
|
+
const versions = new Map();
|
|
9
11
|
export function fetchVault(linked) {
|
|
10
12
|
return linked.client.request("GET", "/api/cli/vault");
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The context key and its version, for a write. The server refuses ciphertext
|
|
16
|
+
* written with a key that was replaced since, so the version comes from the
|
|
17
|
+
* same vault response as the sealed key.
|
|
18
|
+
*/
|
|
19
|
+
export async function writeKey(linked, target) {
|
|
20
|
+
if (!versions.has(target.id))
|
|
21
|
+
memo.delete(target.id);
|
|
22
|
+
const key = await contextKey(linked, { ...target, sealedKey: undefined }, { create: true });
|
|
23
|
+
const keyVersion = versions.get(target.id);
|
|
24
|
+
if (keyVersion === undefined)
|
|
25
|
+
throw new Error(`No key version for ${target.slug}. Run the command again.`);
|
|
26
|
+
return { key, keyVersion };
|
|
27
|
+
}
|
|
12
28
|
/** The context key, unlocking with the Vault password when needed. */
|
|
13
29
|
export async function contextKey(linked, target, opts = {}) {
|
|
14
30
|
const cached = memo.get(target.id);
|
|
@@ -45,10 +61,14 @@ export async function contextKey(linked, target, opts = {}) {
|
|
|
45
61
|
if (!key) {
|
|
46
62
|
throw new Error(member
|
|
47
63
|
? `Waiting for the owner to send the key for ${target.slug}. It arrives the next time they unlock the vault in the dashboard.`
|
|
48
|
-
: `
|
|
64
|
+
: `This account has no key for ${target.slug}. If you reset your vault, reset ${target.slug} in the dashboard to set new values.`);
|
|
49
65
|
}
|
|
50
66
|
for (const [id, k] of opened)
|
|
51
67
|
memo.set(id, k);
|
|
68
|
+
for (const c of vault.contexts) {
|
|
69
|
+
if (opened.has(c.id))
|
|
70
|
+
versions.set(c.id, c.keyVersion);
|
|
71
|
+
}
|
|
52
72
|
if (account)
|
|
53
73
|
await remember(account, vault, opened);
|
|
54
74
|
return key;
|
|
@@ -109,6 +129,7 @@ async function initContextKey(linked, vault, ctx) {
|
|
|
109
129
|
const key = generateContextKey();
|
|
110
130
|
try {
|
|
111
131
|
await linked.client.request("POST", "/api/cli/vault", {
|
|
132
|
+
publicKey: vault.publicKey,
|
|
112
133
|
shares: [{ contextId: ctx.id, sealedKey: sealContextKey(key, vault.publicKey, ctx.id) }],
|
|
113
134
|
});
|
|
114
135
|
}
|
|
@@ -164,7 +185,7 @@ async function holderPrivateKey(linked, vault, opts) {
|
|
|
164
185
|
console.error(pc.red("Wrong Vault password. Try again.\n"));
|
|
165
186
|
continue;
|
|
166
187
|
}
|
|
167
|
-
throw new Error("Could not unlock. Check your Vault password.");
|
|
188
|
+
throw new Error("Could not unlock. Check your Vault password. Forgot it? Reset the vault in the dashboard, Settings.");
|
|
168
189
|
}
|
|
169
190
|
}
|
|
170
191
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getmyenv",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "CLI for getmyenv, an encrypted store for environment variables and secrets. We store ciphertext only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@getmyenv/crypto": "0.4.1",
|
|
37
|
-
"@getmyenv/shared": "0.8.
|
|
37
|
+
"@getmyenv/shared": "0.8.5",
|
|
38
38
|
"@napi-rs/keyring": "^2.1.0",
|
|
39
39
|
"commander": "^15.0.0",
|
|
40
40
|
"dotenv": "^16.4.7",
|