getmyenv 0.6.2 → 0.8.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 (136) 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 +0 -2
  13. package/dist/commands/export.js +12 -15
  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 +10 -16
  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 +7 -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 +4 -8
  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 +14 -6
  39. package/dist/lib/api.js +10 -5
  40. package/dist/lib/authorize.d.ts +0 -1
  41. package/dist/lib/authorize.js +2 -3
  42. package/dist/lib/bootstrap.d.ts +0 -1
  43. package/dist/lib/bootstrap.js +5 -3
  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/run-env.d.ts +2 -4
  61. package/dist/lib/run-env.js +9 -1
  62. package/dist/lib/update-check.d.ts +0 -1
  63. package/dist/lib/update-check.js +4 -2
  64. package/dist/lib/upload.d.ts +0 -2
  65. package/dist/lib/upload.js +11 -20
  66. package/dist/lib/vault.d.ts +54 -0
  67. package/dist/lib/vault.js +174 -0
  68. package/package.json +4 -5
  69. package/dist/bin.d.ts.map +0 -1
  70. package/dist/bin.js.map +0 -1
  71. package/dist/cli.d.ts.map +0 -1
  72. package/dist/cli.js.map +0 -1
  73. package/dist/commands/backup.d.ts.map +0 -1
  74. package/dist/commands/backup.js.map +0 -1
  75. package/dist/commands/claim.d.ts.map +0 -1
  76. package/dist/commands/claim.js.map +0 -1
  77. package/dist/commands/context.d.ts.map +0 -1
  78. package/dist/commands/context.js.map +0 -1
  79. package/dist/commands/export.d.ts.map +0 -1
  80. package/dist/commands/export.js.map +0 -1
  81. package/dist/commands/guest.d.ts.map +0 -1
  82. package/dist/commands/guest.js.map +0 -1
  83. package/dist/commands/import.d.ts.map +0 -1
  84. package/dist/commands/import.js.map +0 -1
  85. package/dist/commands/lock.d.ts.map +0 -1
  86. package/dist/commands/lock.js.map +0 -1
  87. package/dist/commands/ls.d.ts.map +0 -1
  88. package/dist/commands/ls.js.map +0 -1
  89. package/dist/commands/restore.d.ts.map +0 -1
  90. package/dist/commands/restore.js.map +0 -1
  91. package/dist/commands/run.d.ts.map +0 -1
  92. package/dist/commands/run.js.map +0 -1
  93. package/dist/commands/set.d.ts.map +0 -1
  94. package/dist/commands/set.js.map +0 -1
  95. package/dist/commands/start.d.ts.map +0 -1
  96. package/dist/commands/start.js.map +0 -1
  97. package/dist/commands/status.d.ts.map +0 -1
  98. package/dist/commands/status.js.map +0 -1
  99. package/dist/commands/unlink.d.ts.map +0 -1
  100. package/dist/commands/unlink.js.map +0 -1
  101. package/dist/lib/api-url.d.ts.map +0 -1
  102. package/dist/lib/api-url.js.map +0 -1
  103. package/dist/lib/api.d.ts.map +0 -1
  104. package/dist/lib/api.js.map +0 -1
  105. package/dist/lib/authorize.d.ts.map +0 -1
  106. package/dist/lib/authorize.js.map +0 -1
  107. package/dist/lib/bootstrap.d.ts.map +0 -1
  108. package/dist/lib/bootstrap.js.map +0 -1
  109. package/dist/lib/confirm.d.ts.map +0 -1
  110. package/dist/lib/confirm.js.map +0 -1
  111. package/dist/lib/context.d.ts.map +0 -1
  112. package/dist/lib/context.js.map +0 -1
  113. package/dist/lib/credentials.d.ts.map +0 -1
  114. package/dist/lib/credentials.js.map +0 -1
  115. package/dist/lib/fs.d.ts.map +0 -1
  116. package/dist/lib/fs.js.map +0 -1
  117. package/dist/lib/guest-key.d.ts.map +0 -1
  118. package/dist/lib/guest-key.js.map +0 -1
  119. package/dist/lib/identity.d.ts.map +0 -1
  120. package/dist/lib/identity.js.map +0 -1
  121. package/dist/lib/keychain.d.ts.map +0 -1
  122. package/dist/lib/keychain.js.map +0 -1
  123. package/dist/lib/pek.d.ts +0 -27
  124. package/dist/lib/pek.d.ts.map +0 -1
  125. package/dist/lib/pek.js +0 -60
  126. package/dist/lib/pek.js.map +0 -1
  127. package/dist/lib/run-env.d.ts.map +0 -1
  128. package/dist/lib/run-env.js.map +0 -1
  129. package/dist/lib/unlock.d.ts +0 -24
  130. package/dist/lib/unlock.d.ts.map +0 -1
  131. package/dist/lib/unlock.js +0 -83
  132. package/dist/lib/unlock.js.map +0 -1
  133. package/dist/lib/update-check.d.ts.map +0 -1
  134. package/dist/lib/update-check.js.map +0 -1
  135. package/dist/lib/upload.d.ts.map +0 -1
  136. package/dist/lib/upload.js.map +0 -1
@@ -0,0 +1,75 @@
1
+ import pc from "picocolors";
2
+ import { knownKey, knownKeyFormatHint, linkDomain, parseTemplateRef, } from "@getmyenv/shared";
3
+ import { promptHidden } from "../lib/fs.js";
4
+ import { ApiClient, getSessionToken, resolveApiUrl } from "../lib/api.js";
5
+ import { ensureTokenAndLink } from "../lib/bootstrap.js";
6
+ import { resolveContext } from "../lib/context.js";
7
+ import { assertWritable, uploadValues } from "../lib/upload.js";
8
+ /** Load a public template by id or URL. No token needed. */
9
+ export async function fetchTemplate(ref, apiUrl) {
10
+ const publicId = parseTemplateRef(ref);
11
+ if (!publicId) {
12
+ throw new Error(`Not a template id or link: ${ref}`);
13
+ }
14
+ const res = await new ApiClient(resolveApiUrl(apiUrl)).request("GET", `/api/templates/public/${publicId}`);
15
+ return res.template;
16
+ }
17
+ export function printTemplate(template) {
18
+ const n = template.keys.length;
19
+ console.error(`Template ${pc.bold(template.name)} ${pc.dim(`(${n} variable${n === 1 ? "" : "s"})`)}`);
20
+ if (template.description)
21
+ console.error(pc.dim(template.description));
22
+ console.error("");
23
+ }
24
+ /**
25
+ * Add the template's names to the linked project, then ask for each value
26
+ * that is still missing in the folder's default context. Enter skips one.
27
+ */
28
+ export async function applyTemplate(template, opts) {
29
+ if (!getSessionToken())
30
+ throw new Error("Link this folder first: npx getmyenv start");
31
+ const apiUrl = resolveApiUrl(opts.apiUrl);
32
+ await new ApiClient(apiUrl, getSessionToken()).request("POST", "/api/cli/template", {
33
+ publicId: template.publicId,
34
+ });
35
+ const linked = await ensureTokenAndLink({ apiUrl, quiet: true });
36
+ const ctx = resolveContext(linked.who, linked.meta, undefined);
37
+ assertWritable(linked, ctx);
38
+ const status = new Map(linked.who.keys.map((k) => [k.name, k.status[ctx.slug] ?? "missing"]));
39
+ const todo = template.keys.filter((k) => (status.get(k.name) ?? "missing") === "missing");
40
+ if (todo.length === 0) {
41
+ console.error(`${pc.green("✓")} Every template variable has a value in ${ctx.slug}.`);
42
+ return { context: ctx.slug };
43
+ }
44
+ console.error(`Values for ${pc.bold(ctx.slug)}. Hidden as you type. Enter skips one.\n`);
45
+ const pairs = {};
46
+ for (const key of todo) {
47
+ const known = knownKey(key.name);
48
+ const description = key.description || known?.description;
49
+ const link = key.link || known?.docsUrl;
50
+ console.error(`${pc.bold(key.name)}${key.optional ? pc.dim(" (optional)") : ""}`);
51
+ if (description)
52
+ console.error(pc.dim(` ${description}`));
53
+ if (link)
54
+ console.error(pc.dim(` Get it on ${linkDomain(link) ?? link}: ${link}`));
55
+ const value = await promptHidden(` ${key.name}: `);
56
+ if (value) {
57
+ const hint = knownKeyFormatHint(key.name, value);
58
+ if (hint)
59
+ console.error(pc.yellow(` ${hint}`));
60
+ pairs[key.name] = value;
61
+ }
62
+ console.error("");
63
+ }
64
+ if (Object.keys(pairs).length > 0) {
65
+ const res = await uploadValues({ linked, context: ctx, pairs });
66
+ for (const name of res.set)
67
+ console.error(`${pc.green("✓")} ${name} ${pc.dim(`set in ${ctx.slug}`)}`);
68
+ }
69
+ const skipped = todo.filter((k) => !pairs[k.name]).map((k) => k.name);
70
+ if (skipped.length > 0) {
71
+ console.error(pc.dim(`No value yet: ${skipped.join(", ")}. Set later with npx getmyenv set NAME`));
72
+ }
73
+ console.error("");
74
+ return { context: ctx.slug };
75
+ }
@@ -6,4 +6,3 @@ export declare function unlinkCommand(opts: {
6
6
  apiUrl?: string;
7
7
  yes?: boolean;
8
8
  }): Promise<void>;
9
- //# sourceMappingURL=unlink.d.ts.map
@@ -3,8 +3,7 @@ import path from "node:path";
3
3
  import pc from "picocolors";
4
4
  import { getLocalDir, promptConfirm, readClaimFile, readProjectMeta } from "../lib/fs.js";
5
5
  import { clearFolderToken, readFolderToken } from "../lib/credentials.js";
6
- import { clearCachedKey } from "../lib/pek.js";
7
- import { clearCachedPek, keychainAccount } from "../lib/keychain.js";
6
+ import { clearCachedKeys, keychainAccount } from "../lib/keychain.js";
8
7
  import { ApiClient, ApiError, resolveApiUrl } from "../lib/api.js";
9
8
  /**
10
9
  * Unlink this folder: revoke its token and remove local getmyenv files.
@@ -44,19 +43,17 @@ export async function unlinkCommand(opts) {
44
43
  }
45
44
  }
46
45
  const projectId = meta?.projectId ?? folder?.projectId;
47
- if (projectId && (await clearCachedPek(keychainAccount(resolveApiUrl(opts.apiUrl), projectId)))) {
48
- console.log(`${pc.green("✓")} Removed the unlocked key from the OS keychain`);
46
+ if (projectId && (await clearCachedKeys(keychainAccount(resolveApiUrl(opts.apiUrl), projectId)))) {
47
+ console.log(`${pc.green("✓")} Removed the unlocked keys from the OS keychain`);
49
48
  }
50
49
  if (clearFolderToken())
51
50
  console.log(`${pc.green("✓")} Removed token.json`);
52
- if (clearCachedKey())
53
- console.log(`${pc.green("✓")} Removed key.json`);
54
51
  if (fs.existsSync(projectFile)) {
55
52
  fs.unlinkSync(projectFile);
56
53
  console.log(`${pc.green("✓")} Removed project.json`);
57
54
  }
58
55
  if (readClaimFile()) {
59
- console.log(pc.yellow("Kept claim.json. It is the only way to claim this guest project. Delete it if you do not need the project."));
56
+ console.log(pc.yellow("Kept claim.json. It holds the claim code and guest passphrase. Delete it if you do not need the project."));
60
57
  }
61
58
  try {
62
59
  const dir = getLocalDir();
@@ -67,4 +64,3 @@ export async function unlinkCommand(opts) {
67
64
  /* ignore */
68
65
  }
69
66
  }
70
- //# sourceMappingURL=unlink.js.map
@@ -17,4 +17,3 @@ export declare function pickApiUrl(sources: {
17
17
  env?: string;
18
18
  tokenFile?: string;
19
19
  }): string;
20
- //# sourceMappingURL=api-url.d.ts.map
@@ -47,4 +47,3 @@ export function pickApiUrl(sources) {
47
47
  }
48
48
  return normalizeApiBase(raw);
49
49
  }
50
- //# sourceMappingURL=api-url.js.map
package/dist/lib/api.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * CLI API client. Never prints plaintext values.
3
3
  */
4
- /** Keep in sync with packages/cli/package.json version. */
5
- export declare const CLI_VERSION = "0.6.2";
4
+ import type { NewMachinePolicy } from "@getmyenv/shared";
5
+ /** From package.json, which ships with the package: src/lib and dist/lib are both two levels down. */
6
+ export declare const CLI_VERSION: string;
6
7
  export { isDefaultApiUrl, parseSafeUrl } from "./api-url.js";
7
8
  export type Protection = "open" | "locked";
8
9
  export type ValueStatus = "set" | "expiring_soon" | "expired" | "missing";
@@ -16,14 +17,19 @@ export type WhoamiResponse = {
16
17
  project: {
17
18
  id: string;
18
19
  name: string;
19
- hasKey: boolean;
20
20
  };
21
+ /** A Vault password (or guest passphrase) is set for this token's holder. */
22
+ vaultSet: boolean;
21
23
  token: {
22
24
  kind: "folder" | "guest" | "server";
23
25
  label: string | null;
24
- /** Server tokens: the one locked context they read. */
26
+ /** Server tokens: the one read-only context they read. */
25
27
  context: string | null;
26
28
  allowedCidrs: string[];
29
+ /** Server tokens: what happens when an untrusted machine reads. Null for other tokens. */
30
+ newMachinePolicy: NewMachinePolicy | null;
31
+ /** Folder token of a member on a shared project. Sees only the contexts shared with them. */
32
+ member?: boolean;
27
33
  };
28
34
  user: {
29
35
  email: string;
@@ -48,6 +54,8 @@ export declare class ApiClient {
48
54
  private headers;
49
55
  request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
50
56
  }
51
- /** Env / folder token only, no prompt. */
57
+ /**
58
+ * Env / folder token only, no prompt. A server token is `auth.privateKey`:
59
+ * only the auth part is sent. The private key never leaves this machine.
60
+ */
52
61
  export declare function getSessionToken(): string | undefined;
53
- //# sourceMappingURL=api.d.ts.map
package/dist/lib/api.js CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * CLI API client. Never prints plaintext values.
3
3
  */
4
+ import { createRequire } from "node:module";
4
5
  import { readFolderToken } from "./credentials.js";
5
6
  import { pickApiUrl } from "./api-url.js";
6
- /** Keep in sync with packages/cli/package.json version. */
7
- export const CLI_VERSION = "0.6.2";
7
+ /** From package.json, which ships with the package: src/lib and dist/lib are both two levels down. */
8
+ export const CLI_VERSION = createRequire(import.meta.url)("../../package.json").version;
8
9
  export { isDefaultApiUrl, parseSafeUrl } from "./api-url.js";
9
10
  export class ApiError extends Error {
10
11
  status;
@@ -46,6 +47,8 @@ export class ApiClient {
46
47
  method,
47
48
  headers: this.headers(extraHeaders),
48
49
  body: body === undefined ? undefined : JSON.stringify(body),
50
+ // The token must never follow a redirect to another host.
51
+ redirect: "error",
49
52
  });
50
53
  }
51
54
  catch {
@@ -68,11 +71,13 @@ export class ApiClient {
68
71
  return (await res.json());
69
72
  }
70
73
  }
71
- /** Env / folder token only, no prompt. */
74
+ /**
75
+ * Env / folder token only, no prompt. A server token is `auth.privateKey`:
76
+ * only the auth part is sent. The private key never leaves this machine.
77
+ */
72
78
  export function getSessionToken() {
73
79
  const env = process.env.GETMYENV_TOKEN?.trim();
74
80
  if (env)
75
- return env;
81
+ return env.split(".")[0];
76
82
  return readFolderToken()?.token;
77
83
  }
78
- //# sourceMappingURL=api.js.map
@@ -13,4 +13,3 @@ export declare function sleep(ms: number): Promise<void>;
13
13
  export declare function authorizeFolder(opts?: {
14
14
  apiUrl?: string;
15
15
  }): Promise<WhoamiResponse>;
16
- //# sourceMappingURL=authorize.d.ts.map
@@ -62,7 +62,7 @@ async function saveVerifiedToken(token, apiUrl) {
62
62
  async function authorizeViaPaste(apiUrl) {
63
63
  const cliPage = `${apiUrl.replace(/\/$/, "")}/cli`;
64
64
  console.error(pc.dim(`Create or copy a folder token: ${cliPage}\n`));
65
- const token = (await readLine("Paste CLI token: ")).trim();
65
+ const token = (await readLine("Paste the folder token: ")).trim();
66
66
  if (!token) {
67
67
  throw new Error("No token entered.");
68
68
  }
@@ -122,8 +122,7 @@ async function authorizeViaBrowser(apiUrl) {
122
122
  */
123
123
  export async function authorizeFolder(opts = {}) {
124
124
  if (!process.stdin.isTTY) {
125
- throw new Error("No CLI token in this folder. Set GETMYENV_TOKEN for CI, or run in a terminal.");
125
+ throw new Error("No token in this folder. Run in a terminal to sign in, or set GETMYENV_TOKEN to a server token.");
126
126
  }
127
127
  return authorizeViaBrowser(resolveApiUrl(opts.apiUrl));
128
128
  }
129
- //# sourceMappingURL=authorize.js.map
@@ -19,4 +19,3 @@ export declare function ensureTokenAndLink(opts?: {
19
19
  apiUrl?: string;
20
20
  quiet?: boolean;
21
21
  }): Promise<Linked>;
22
- //# sourceMappingURL=bootstrap.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import pc from "picocolors";
2
2
  import { ensureGitignoreHasEnv, readClaimFile, readProjectMeta, writeProjectMeta, } from "./fs.js";
3
3
  import { ApiClient, ApiError, getSessionToken, resolveApiUrl, } from "./api.js";
4
+ import { assertTokenNotCommitted } from "./credentials.js";
4
5
  export const NOT_LINKED_STEPS = [
5
6
  "This folder is not linked to a getmyenv project.",
6
7
  "Start in a terminal: npx getmyenv",
@@ -12,11 +13,11 @@ export function printGuestBanner(who) {
12
13
  return;
13
14
  const DAY = 24 * 60 * 60 * 1000;
14
15
  const expires = who.guestExpiresAt ? new Date(who.guestExpiresAt).getTime() : NaN;
15
- const needClaim = readClaimFile() ? "" : " Needs .getmyenv/claim.json.";
16
+ const needClaim = readClaimFile() ? "" : " Needs the claim code and guest passphrase shown at creation.";
16
17
  const plural = (n) => (n === 1 ? "1 day" : `${n} days`);
17
18
  if (Number.isFinite(expires) && expires <= Date.now()) {
18
19
  const graceLeft = Math.max(0, Math.ceil((expires + 30 * DAY - Date.now()) / DAY));
19
- console.error(pc.yellow(`Guest project expired. Read-only for ${plural(graceLeft)}. Export or claim it: npx getmyenv claim.${needClaim}\n`));
20
+ console.error(pc.yellow(`Guest project expired. Values can't change. run works for ${plural(graceLeft)} more. Export or claim it: npx getmyenv claim.${needClaim}\n`));
20
21
  return;
21
22
  }
22
23
  const daysLeft = Number.isFinite(expires) ? Math.max(0, Math.ceil((expires - Date.now()) / DAY)) : null;
@@ -55,6 +56,8 @@ export async function ensureTokenAndLink(opts = {}) {
55
56
  const token = getSessionToken();
56
57
  if (!token)
57
58
  throw new Error(NOT_LINKED_STEPS);
59
+ if (!process.env.GETMYENV_TOKEN?.trim())
60
+ assertTokenNotCommitted();
58
61
  const apiUrl = resolveApiUrl(opts.apiUrl);
59
62
  const client = new ApiClient(apiUrl, token);
60
63
  const who = await fetchWhoami(client);
@@ -79,4 +82,3 @@ export async function ensureTokenAndLink(opts = {}) {
79
82
  }
80
83
  return { who, apiUrl, token, client, meta };
81
84
  }
82
- //# sourceMappingURL=bootstrap.js.map
@@ -14,4 +14,3 @@ export declare function confirmDiff(opts: {
14
14
  /** Skip the prompt when true (still returns false if conflicts and non-TTY without yes). */
15
15
  yes?: boolean;
16
16
  }): Promise<boolean>;
17
- //# sourceMappingURL=confirm.d.ts.map
@@ -23,4 +23,3 @@ export async function confirmDiff(opts) {
23
23
  }
24
24
  return promptConfirm(`${action}?`, { defaultYes: false });
25
25
  }
26
- //# sourceMappingURL=confirm.js.map
@@ -8,5 +8,6 @@ export declare function defaultContextSlug(who: Pick<WhoamiResponse, "token" | "
8
8
  /** Find a context by slug, or by name ("Customer Acme" finds customer-acme). */
9
9
  export declare function findContext(contexts: ContextInfo[], input: string): ContextInfo | null;
10
10
  /** The context a command acts on. Throws with the available slugs when unknown. */
11
- export declare function resolveContext(who: Pick<WhoamiResponse, "token" | "contexts">, meta: ProjectMeta | null, explicit?: string): ContextInfo;
12
- //# sourceMappingURL=context.d.ts.map
11
+ export declare function resolveContext(who: Pick<WhoamiResponse, "token" | "contexts"> & {
12
+ isGuest?: boolean;
13
+ }, meta: ProjectMeta | null, explicit?: string): ContextInfo;
@@ -38,6 +38,8 @@ export function resolveContext(who, meta, explicit) {
38
38
  throw new Error(`This server token reads ${who.token.context ?? "one read-only context"} only.`);
39
39
  }
40
40
  const available = who.contexts.map((c) => c.slug).join(", ") || "none";
41
- throw new Error(`No context "${wanted}". Available: ${available}\nCreate one: npx getmyenv context create "${wanted}"`);
41
+ const next = who.isGuest
42
+ ? "Guest projects have Development only. Claim it to add contexts: npx getmyenv claim"
43
+ : `Create one: npx getmyenv context create "${wanted}"`;
44
+ throw new Error(`No context "${wanted}". Available: ${available}\n${next}`);
42
45
  }
43
- //# sourceMappingURL=context.js.map
@@ -1,3 +1,7 @@
1
+ /** True when git tracks this file. False without git or outside a repo. */
2
+ export declare function isTrackedByGit(file: string, cwd?: string): boolean;
3
+ /** A committed token.json is readable by anyone with the repo. Refuse to use it. */
4
+ export declare function assertTokenNotCommitted(cwd?: string): void;
1
5
  /**
2
6
  * Per-folder token stored in `.getmyenv/token.json` (gitignored).
3
7
  * One folder = one project = one token. What the token can reach is decided
@@ -19,4 +23,3 @@ export declare function writeFolderToken(entry: {
19
23
  addedAt?: string;
20
24
  }, cwd?: string): string;
21
25
  export declare function clearFolderToken(cwd?: string): boolean;
22
- //# sourceMappingURL=credentials.d.ts.map
@@ -1,7 +1,33 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
- import { getLocalDir } from "./fs.js";
4
+ import { getLocalDir, getProjectRoot } from "./fs.js";
4
5
  import { apiUrlForDisk } from "./api-url.js";
6
+ /** True when git tracks this file. False without git or outside a repo. */
7
+ export function isTrackedByGit(file, cwd = process.cwd()) {
8
+ try {
9
+ const res = spawnSync("git", ["ls-files", "--error-unmatch", "--", file], {
10
+ cwd: getProjectRoot(cwd),
11
+ stdio: "ignore",
12
+ timeout: 5000,
13
+ });
14
+ return res.status === 0;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /** A committed token.json is readable by anyone with the repo. Refuse to use it. */
21
+ export function assertTokenNotCommitted(cwd = process.cwd()) {
22
+ const file = tokenFilePath(cwd);
23
+ if (fs.existsSync(file) && isTrackedByGit(file, cwd)) {
24
+ throw new Error([
25
+ ".getmyenv/token.json is committed to git. Anyone with the repo can use it.",
26
+ "Revoke it on the CLI page, then: git rm --cached .getmyenv/token.json",
27
+ "Then run: npx getmyenv unlink, and npx getmyenv",
28
+ ].join("\n"));
29
+ }
30
+ }
5
31
  export function tokenFilePath(cwd = process.cwd()) {
6
32
  return path.join(getLocalDir(cwd), "token.json");
7
33
  }
@@ -51,6 +77,7 @@ export function writeFolderToken(entry, cwd = process.cwd()) {
51
77
  fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", {
52
78
  mode: 0o600,
53
79
  });
80
+ fs.chmodSync(file, 0o600);
54
81
  return file;
55
82
  }
56
83
  export function clearFolderToken(cwd = process.cwd()) {
@@ -60,4 +87,3 @@ export function clearFolderToken(cwd = process.cwd()) {
60
87
  fs.unlinkSync(file);
61
88
  return true;
62
89
  }
63
- //# sourceMappingURL=credentials.js.map
package/dist/lib/fs.d.ts CHANGED
@@ -16,6 +16,13 @@ export declare function getProjectRoot(cwd?: string): string;
16
16
  export declare function getLocalDir(cwd?: string): string;
17
17
  export declare function readProjectMeta(cwd?: string): ProjectMeta | null;
18
18
  export declare function writeProjectMeta(meta: ProjectMeta, cwd?: string): void;
19
+ /**
20
+ * Write a file only this user can read. Never through a symlink. A new file
21
+ * is created with `wx`; an existing one is replaced only when `overwrite`.
22
+ */
23
+ export declare function writePrivateFile(file: string, content: string, opts: {
24
+ overwrite: boolean;
25
+ }): void;
19
26
  /** OS user config for machine identity - never in the repo. */
20
27
  export declare function getConfigDir(): string;
21
28
  /**
@@ -24,13 +31,10 @@ export declare function getConfigDir(): string;
24
31
  */
25
32
  export declare function readLine(question: string): Promise<string>;
26
33
  /**
27
- * Read a secret from stdin (TTY-hidden, or piped via --vault-password-stdin).
28
- * Refuses to echo secrets on a non-TTY unless allowStdin is true.
34
+ * Read a secret at a terminal without echo. Refuses without a terminal.
29
35
  * Always restores stdin so a later promptConfirm / promptChoice can run.
30
36
  */
31
- export declare function promptHidden(question: string, opts?: {
32
- allowStdin?: boolean;
33
- }): Promise<string>;
37
+ export declare function promptHidden(question: string): Promise<string>;
34
38
  /** Yes/no on a TTY. Empty input picks the default, which is No unless `defaultYes`. */
35
39
  export declare function promptConfirm(question: string, opts?: {
36
40
  defaultYes?: boolean;
@@ -43,7 +47,7 @@ export declare function ensureGitignoreHas(file: string, cwd?: string): void;
43
47
  export type ClaimFile = {
44
48
  version: 1;
45
49
  claimCode: string;
46
- /** Guest wrapping passphrase. Lives in the repo until claim. */
50
+ /** Guest wrapping passphrase. Local and gitignored until claim. */
47
51
  passphrase: string;
48
52
  projectId: string;
49
53
  projectName: string;
@@ -73,4 +77,3 @@ export declare function rootEnvFiles(cwd?: string): string[];
73
77
  * Sample files map to null too.
74
78
  */
75
79
  export declare function contextSlugFromEnvFile(filePath: string): string | null;
76
- //# sourceMappingURL=fs.d.ts.map
package/dist/lib/fs.js CHANGED
@@ -67,6 +67,27 @@ export function writeProjectMeta(meta, cwd = process.cwd()) {
67
67
  };
68
68
  fs.writeFileSync(path.join(dir, "project.json"), JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
69
69
  }
70
+ /**
71
+ * Write a file only this user can read. Never through a symlink. A new file
72
+ * is created with `wx`; an existing one is replaced only when `overwrite`.
73
+ */
74
+ export function writePrivateFile(file, content, opts) {
75
+ let stat = null;
76
+ try {
77
+ stat = fs.lstatSync(file);
78
+ }
79
+ catch {
80
+ /* does not exist */
81
+ }
82
+ if (stat?.isSymbolicLink())
83
+ throw new Error(`${file} is a symlink. Refusing to write through it.`);
84
+ if (stat && !stat.isFile())
85
+ throw new Error(`${file} is not a regular file.`);
86
+ if (stat && !opts.overwrite)
87
+ throw new Error(`${file} already exists. Pick another name with -o.`);
88
+ fs.writeFileSync(file, content, { mode: 0o600, flag: stat ? "w" : "wx" });
89
+ fs.chmodSync(file, 0o600);
90
+ }
70
91
  /** OS user config for machine identity - never in the repo. */
71
92
  export function getConfigDir() {
72
93
  const base = process.env.GETMYENV_CONFIG_DIR?.trim() ||
@@ -75,11 +96,12 @@ export function getConfigDir() {
75
96
  return base;
76
97
  }
77
98
  /**
78
- * Leave stdin as we found it after raw-mode secret input: cooked mode, no
79
- * listeners, paused. A flowing stdin keeps the process alive and, in `run`,
80
- * reads keystrokes meant for the child. readline resumes it when needed.
99
+ * Leave stdin as we found it after raw-mode secret input: cooked mode, paused.
100
+ * A flowing stdin keeps the process alive and, in `run`, reads keystrokes meant
101
+ * for the child. readline resumes it when needed. Remove only our own listener:
102
+ * readline adds its keypress listener once per stream and never again.
81
103
  */
82
- function restoreStdinAfterRaw() {
104
+ function restoreStdinAfterRaw(onData) {
83
105
  const stdin = process.stdin;
84
106
  try {
85
107
  stdin.setRawMode?.(false);
@@ -87,7 +109,8 @@ function restoreStdinAfterRaw() {
87
109
  catch {
88
110
  /* ignore */
89
111
  }
90
- stdin.removeAllListeners("data");
112
+ if (onData)
113
+ stdin.removeListener("data", onData);
91
114
  stdin.pause();
92
115
  }
93
116
  /**
@@ -113,21 +136,13 @@ export function readLine(question) {
113
136
  });
114
137
  }
115
138
  /**
116
- * Read a secret from stdin (TTY-hidden, or piped via --vault-password-stdin).
117
- * Refuses to echo secrets on a non-TTY unless allowStdin is true.
139
+ * Read a secret at a terminal without echo. Refuses without a terminal.
118
140
  * Always restores stdin so a later promptConfirm / promptChoice can run.
119
141
  */
120
- export async function promptHidden(question, opts) {
142
+ export async function promptHidden(question) {
121
143
  const stdin = process.stdin;
122
144
  if (!stdin.isTTY) {
123
- if (!opts?.allowStdin) {
124
- throw new Error("No terminal to ask for the Vault password. Unlock once in a terminal (open contexts stay unlocked for 8 hours), or pipe it with --vault-password-stdin. With set --stdin, send the password on the first line and the value after it.");
125
- }
126
- const chunks = [];
127
- for await (const chunk of stdin) {
128
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
129
- }
130
- return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
145
+ throw new Error("No terminal to ask for the Vault password. Unlock once in a terminal: open contexts stay unlocked for 8 hours. On servers and in CI, use a server token.");
131
146
  }
132
147
  // Prompts go to stderr so stdout stays clean for `export --stdout > .env` and pipes.
133
148
  const out = process.stderr;
@@ -141,13 +156,13 @@ export async function promptHidden(question, opts) {
141
156
  return;
142
157
  for (const c of text) {
143
158
  if (c === "\n" || c === "\r" || c === "\u0004") {
144
- restoreStdinAfterRaw();
159
+ restoreStdinAfterRaw(onData);
145
160
  out.write("\n");
146
161
  resolve(value);
147
162
  return;
148
163
  }
149
164
  if (c === "\u0003") {
150
- restoreStdinAfterRaw();
165
+ restoreStdinAfterRaw(onData);
151
166
  out.write("\n");
152
167
  process.exitCode = 130;
153
168
  reject(new Error("Cancelled."));
@@ -205,14 +220,14 @@ export async function promptChoice(question, options, defaultIndex = 0) {
205
220
  }
206
221
  export function ensureGitignoreHasEnv(cwd = process.cwd()) {
207
222
  const gi = path.join(getProjectRoot(cwd), ".gitignore");
208
- // Ignore bearer + cached key only. project.json and claim.json are committed for handoff.
209
- // claim.json holds the guest wrapping passphrase until claim - prefer private repos until then.
223
+ // project.json is committed so a clone knows its project. The token and the
224
+ // guest passphrase in claim.json stay on this machine.
210
225
  const entries = [
211
226
  ".env",
212
227
  ".env.local",
213
228
  ".env.*.local",
214
229
  ".getmyenv/token.json",
215
- ".getmyenv/key.json",
230
+ ".getmyenv/claim.json",
216
231
  ];
217
232
  let content = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : "";
218
233
  let changed = false;
@@ -276,6 +291,7 @@ export function writeClaimFile(data, cwd = process.cwd()) {
276
291
  fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", {
277
292
  mode: 0o600,
278
293
  });
294
+ fs.chmodSync(file, 0o600);
279
295
  return file;
280
296
  }
281
297
  export function clearClaimFile(cwd = process.cwd()) {
@@ -418,4 +434,3 @@ export function contextSlugFromEnvFile(filePath) {
418
434
  const slug = slugify(m[1]);
419
435
  return isValidContextSlug(slug) ? slug : null;
420
436
  }
421
- //# sourceMappingURL=fs.js.map
@@ -1,7 +1,8 @@
1
+ /** Ask for a new Vault password twice. Same rule as the dashboard. */
2
+ export declare function promptNewVaultPassword(): Promise<string>;
1
3
  export type ResolveVaultPasswordOpts = {
2
4
  /** From whoami / ensureTokenAndLink. */
3
5
  isGuest?: boolean;
4
- vaultPasswordStdin?: boolean;
5
6
  /** Prompt label when a password is needed (unlock path). */
6
7
  prompt?: string;
7
8
  /**
@@ -10,7 +11,9 @@ export type ResolveVaultPasswordOpts = {
10
11
  */
11
12
  create?: boolean;
12
13
  };
13
- /** Password already read from stdin by the command (for example `set --stdin`). */
14
- export declare function presetVaultPassword(password: string): void;
14
+ /**
15
+ * Guest projects wrap the guest private key with the passphrase in claim.json.
16
+ * Use that silently so guest, set and run need no Vault password prompt.
17
+ * Claimed projects prompt for the Vault password in a terminal.
18
+ */
15
19
  export declare function resolveVaultPassword(opts: ResolveVaultPasswordOpts): Promise<string>;
16
- //# sourceMappingURL=guest-key.d.ts.map
@@ -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