getmyenv 0.9.8 → 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 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.8):
38
+ Output of `npx getmyenv --help` (0.9.9):
39
39
 
40
40
  ```text
41
41
  Usage: getmyenv [options] [command]
@@ -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 { contextKey } from "../lib/vault.js";
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 contextKey(linked, target, { create: true });
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}`);
@@ -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 { contextKey } from "./vault.js";
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 key = await contextKey(linked, context, { create: true });
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
  };
@@ -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
- : `No key for ${target.slug} on this account. Open the project in the dashboard once.`);
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.8",
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.4",
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",