getmyenv 0.7.0 → 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.
- package/README.md +18 -16
- 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 +0 -2
- package/dist/commands/export.js +12 -15
- 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 +7 -16
- 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 +4 -8
- 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 +2 -3
- package/dist/lib/bootstrap.d.ts +0 -1
- package/dist/lib/bootstrap.js +5 -3
- 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/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 +4 -5
- 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
|
@@ -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
|
+
}
|
package/dist/commands/unlink.js
CHANGED
|
@@ -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 {
|
|
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
|
|
48
|
-
console.log(`${pc.green("✓")} Removed the unlocked
|
|
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
|
|
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
|
package/dist/lib/api-url.d.ts
CHANGED
package/dist/lib/api-url.js
CHANGED
package/dist/lib/api.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* CLI API client. Never prints plaintext values.
|
|
3
3
|
*/
|
|
4
4
|
import type { NewMachinePolicy } from "@getmyenv/shared";
|
|
5
|
-
/**
|
|
6
|
-
export declare const CLI_VERSION
|
|
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;
|
|
7
7
|
export { isDefaultApiUrl, parseSafeUrl } from "./api-url.js";
|
|
8
8
|
export type Protection = "open" | "locked";
|
|
9
9
|
export type ValueStatus = "set" | "expiring_soon" | "expired" | "missing";
|
|
@@ -17,16 +17,19 @@ export type WhoamiResponse = {
|
|
|
17
17
|
project: {
|
|
18
18
|
id: string;
|
|
19
19
|
name: string;
|
|
20
|
-
hasKey: boolean;
|
|
21
20
|
};
|
|
21
|
+
/** A Vault password (or guest passphrase) is set for this token's holder. */
|
|
22
|
+
vaultSet: boolean;
|
|
22
23
|
token: {
|
|
23
24
|
kind: "folder" | "guest" | "server";
|
|
24
25
|
label: string | null;
|
|
25
|
-
/** Server tokens: the one
|
|
26
|
+
/** Server tokens: the one read-only context they read. */
|
|
26
27
|
context: string | null;
|
|
27
28
|
allowedCidrs: string[];
|
|
28
|
-
/** Server tokens: what happens when an untrusted machine reads.
|
|
29
|
-
newMachinePolicy
|
|
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;
|
|
30
33
|
};
|
|
31
34
|
user: {
|
|
32
35
|
email: string;
|
|
@@ -51,6 +54,8 @@ export declare class ApiClient {
|
|
|
51
54
|
private headers;
|
|
52
55
|
request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
|
|
53
56
|
}
|
|
54
|
-
/**
|
|
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
|
+
*/
|
|
55
61
|
export declare function getSessionToken(): string | undefined;
|
|
56
|
-
//# 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
|
-
/**
|
|
7
|
-
export const CLI_VERSION = "
|
|
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
|
-
/**
|
|
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
|
package/dist/lib/authorize.d.ts
CHANGED
package/dist/lib/authorize.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
package/dist/lib/bootstrap.d.ts
CHANGED
package/dist/lib/bootstrap.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
package/dist/lib/confirm.d.ts
CHANGED
package/dist/lib/confirm.js
CHANGED
package/dist/lib/context.d.ts
CHANGED
|
@@ -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"
|
|
12
|
-
|
|
11
|
+
export declare function resolveContext(who: Pick<WhoamiResponse, "token" | "contexts"> & {
|
|
12
|
+
isGuest?: boolean;
|
|
13
|
+
}, meta: ProjectMeta | null, explicit?: string): ContextInfo;
|
package/dist/lib/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/lib/credentials.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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,
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
142
|
+
export async function promptHidden(question) {
|
|
121
143
|
const stdin = process.stdin;
|
|
122
144
|
if (!stdin.isTTY) {
|
|
123
|
-
|
|
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
|
-
//
|
|
209
|
-
//
|
|
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/
|
|
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
|
package/dist/lib/guest-key.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
14
|
-
|
|
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
|
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
|