getmyenv 0.9.5 → 0.9.7

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
@@ -22,7 +22,7 @@ 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. Reads `.getmyenv/claim.json`, or asks for the claim code and guest passphrase that `guest` printed once.
25
+ - `claim`: claim a guest project. Reads `.getmyenv/claim.json`, or asks for the claim code and guest passphrase that `guest` printed once at a terminal.
26
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
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
28
  - `start`: same as bare `getmyenv`. `start --template <id or link>` adds a template's variable names and asks for each value.
@@ -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.5):
38
+ Output of `npx getmyenv --help` (0.9.7):
39
39
 
40
40
  ```text
41
41
  Usage: getmyenv [options] [command]
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
4
  import { createUserKeys, wipe } from "@getmyenv/crypto";
5
+ import { FACTS } from "@getmyenv/shared";
5
6
  import { ensureGitignoreHasEnv, readProjectMeta, writeClaimFile, writeProjectMeta, } from "../lib/fs.js";
6
7
  import { readFolderToken, writeFolderToken } from "../lib/credentials.js";
7
8
  import { ApiClient, resolveApiUrl } from "../lib/api.js";
@@ -50,9 +51,14 @@ export async function createGuestProject(opts = {}) {
50
51
  const daysLeft = Math.max(0, Math.ceil((new Date(created.guestExpiresAt).getTime() - Date.now()) / 86_400_000));
51
52
  console.error(`${pc.green("✓")} Guest project ${pc.bold(created.projectName)}. Context: development.`);
52
53
  console.error(pc.dim(`Expires in ${daysLeft} days unless claimed. Commit .getmyenv/project.json. token.json and claim.json stay local.`));
53
- console.error(pc.dim("claim.json holds the claim code and guest passphrase. Shown once, save them to claim later:"));
54
- console.error(` ${pc.dim("Claim code:")} ${created.claimCode}`);
55
- console.error(` ${pc.dim("Guest passphrase:")} ${passphrase}\n`);
54
+ if (process.stdin.isTTY) {
55
+ console.error(pc.dim("claim.json holds the claim code and guest passphrase. Shown once, save them to claim later:"));
56
+ console.error(` ${pc.dim("Claim code:")} ${created.claimCode}`);
57
+ console.error(` ${pc.dim("Guest passphrase:")} ${passphrase}\n`);
58
+ }
59
+ else {
60
+ console.error(pc.dim(`claim.json holds the claim code and guest passphrase. It is the only copy. Keep it private. ${FACTS.claimLoss}\n`));
61
+ }
56
62
  return created;
57
63
  }
58
64
  export async function guestCommand(opts = {}) {
package/dist/lib/api.d.ts CHANGED
@@ -5,6 +5,7 @@ import type { NewMachinePolicy } from "@getmyenv/shared";
5
5
  /** From package.json, which ships with the package: src/lib and dist/lib are both two levels down. */
6
6
  export declare const CLI_VERSION: string;
7
7
  export { isDefaultApiUrl, parseSafeUrl } from "./api-url.js";
8
+ export declare const API_TIMEOUT_MS = 30000;
8
9
  export type Protection = "open" | "locked";
9
10
  export type ValueStatus = "set" | "expiring_soon" | "expired" | "missing";
10
11
  export type ContextInfo = {
package/dist/lib/api.js CHANGED
@@ -7,6 +7,7 @@ import { pickApiUrl } from "./api-url.js";
7
7
  /** From package.json, which ships with the package: src/lib and dist/lib are both two levels down. */
8
8
  export const CLI_VERSION = createRequire(import.meta.url)("../../package.json").version;
9
9
  export { isDefaultApiUrl, parseSafeUrl } from "./api-url.js";
10
+ export const API_TIMEOUT_MS = 30_000;
10
11
  export class ApiError extends Error {
11
12
  status;
12
13
  constructor(message, status) {
@@ -49,27 +50,41 @@ export class ApiClient {
49
50
  body: body === undefined ? undefined : JSON.stringify(body),
50
51
  // The token must never follow a redirect to another host.
51
52
  redirect: "error",
53
+ signal: AbortSignal.timeout(API_TIMEOUT_MS),
52
54
  });
53
55
  }
54
- catch {
55
- throw new ApiError(`Could not reach ${this.baseUrl}`, 0);
56
+ catch (err) {
57
+ throw networkError(err, `Could not reach ${this.baseUrl}`);
56
58
  }
57
59
  if (!res.ok) {
58
- let message = `HTTP ${res.status}`;
60
+ let serverMessage;
59
61
  try {
60
62
  const data = (await res.json());
61
- if (data.error)
62
- message = data.error;
63
+ serverMessage = data.error;
63
64
  }
64
65
  catch {
65
66
  /* ignore */
66
67
  }
68
+ const message = res.status >= 500
69
+ ? `The getmyenv server had an error (HTTP ${res.status}). Try again in a moment.`
70
+ : (serverMessage ?? `HTTP ${res.status}`);
67
71
  throw new ApiError(message, res.status);
68
72
  }
69
73
  if (res.status === 204)
70
74
  return undefined;
71
- return (await res.json());
75
+ try {
76
+ return (await res.json());
77
+ }
78
+ catch (err) {
79
+ throw networkError(err, `The connection to ${this.baseUrl} closed before the answer arrived. Try again in a moment.`);
80
+ }
81
+ }
82
+ }
83
+ function networkError(err, fallback) {
84
+ if (err instanceof Error && err.name === "TimeoutError") {
85
+ return new ApiError(`The getmyenv server did not answer within ${API_TIMEOUT_MS / 1000} seconds. Try again in a moment.`, 0);
72
86
  }
87
+ return new ApiError(fallback, 0);
73
88
  }
74
89
  /**
75
90
  * Env / folder token only, no prompt. A server token is `auth.privateKey`:
@@ -35,8 +35,8 @@ export async function fetchWhoami(client) {
35
35
  }
36
36
  if (err instanceof ApiError && err.status === 401) {
37
37
  throw new Error(process.env.GETMYENV_TOKEN?.trim()
38
- ? `${err.message}\nGETMYENV_TOKEN no longer works. Create a new token in Project settings, Tokens.`
39
- : `${err.message}\nThis folder's token no longer works. Run: npx getmyenv unlink, then npx getmyenv`);
38
+ ? "GETMYENV_TOKEN no longer works. It was revoked or expired. Create a new token in Project settings, Tokens."
39
+ : "This folder's token no longer works. It was revoked or expired. Run: npx getmyenv unlink, then npx getmyenv");
40
40
  }
41
41
  throw err;
42
42
  }
package/dist/lib/fs.js CHANGED
@@ -142,7 +142,7 @@ export function readLine(question) {
142
142
  export async function promptHidden(question) {
143
143
  const stdin = process.stdin;
144
144
  if (!stdin.isTTY) {
145
- throw new Error(`No terminal to ask for the Vault password. Unlock once in a terminal: open contexts stay unlocked for ${VAULT_UNLOCK_DURATION}. On servers and in CI, use a server token.`);
145
+ throw new Error(`No terminal to ask for the Vault password. Run this same command once in a terminal to unlock. Open contexts then stay unlocked for ${VAULT_UNLOCK_DURATION}. On servers and in CI, use a server token.`);
146
146
  }
147
147
  // Prompts go to stderr so stdout stays clean for `export --stdout > .env` and pipes.
148
148
  const out = process.stderr;
@@ -1,4 +1,4 @@
1
- import type { ContextInfo } from "./api.js";
1
+ import { type ContextInfo } from "./api.js";
2
2
  import type { Linked } from "./bootstrap.js";
3
3
  /** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
4
4
  export declare function assertWritable(linked: Linked, ctx: ContextInfo): void;
@@ -1,5 +1,6 @@
1
1
  import { encryptSecret, serializePayload } from "@getmyenv/crypto";
2
2
  import { FACTS } from "@getmyenv/shared";
3
+ import { ApiError } from "./api.js";
3
4
  import { contextKey } from "./vault.js";
4
5
  /** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
5
6
  export function assertWritable(linked, ctx) {
@@ -39,7 +40,15 @@ export async function uploadValues(input) {
39
40
  if (Buffer.byteLength(JSON.stringify(body)) > UPLOAD_MAX_BYTES) {
40
41
  throw new Error("Too much to upload at once (max 1 MB encrypted). Split the file and import each part.");
41
42
  }
42
- return linked.client.request("POST", "/api/cli/values", body);
43
+ try {
44
+ return await linked.client.request("POST", "/api/cli/values", body);
45
+ }
46
+ catch (err) {
47
+ if (err instanceof ApiError && err.status >= 500) {
48
+ throw new ApiError(`${err.message} Some values may already be saved. npx getmyenv ls shows what is set in ${context.slug}.`, err.status);
49
+ }
50
+ throw err;
51
+ }
43
52
  }
44
53
  /** Matches the server limit on `/api/cli/values`. */
45
54
  const UPLOAD_MAX_BYTES = 1024 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getmyenv",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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.1",
37
+ "@getmyenv/shared": "0.8.3",
38
38
  "@napi-rs/keyring": "^2.1.0",
39
39
  "commander": "^15.0.0",
40
40
  "dotenv": "^16.4.7",