envspot 0.1.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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/auth-store.js +67 -0
- package/dist/cli-api-error.js +48 -0
- package/dist/config.js +45 -0
- package/dist/device-flow.js +106 -0
- package/dist/device-login.js +18 -0
- package/dist/dump.js +37 -0
- package/dist/env-parse.js +61 -0
- package/dist/errors.js +84 -0
- package/dist/fly-deploy.js +158 -0
- package/dist/http.js +84 -0
- package/dist/index.js +295 -0
- package/dist/init-flow.js +411 -0
- package/dist/init-lock.js +64 -0
- package/dist/link-flow.js +140 -0
- package/dist/open-browser.js +29 -0
- package/dist/output.js +21 -0
- package/dist/paths.js +68 -0
- package/dist/project-detect.js +88 -0
- package/dist/run-handler.js +94 -0
- package/dist/secrets.js +50 -0
- package/dist/token-kind.js +14 -0
- package/dist/whoami.js +93 -0
- package/package.json +52 -0
package/dist/paths.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
export const LINK_PRIMARY = ".envspot.json";
|
|
4
|
+
export const LINK_LEGACY = ".envspot";
|
|
5
|
+
/** Mongo ObjectId hex (24 chars). Project ids are ObjectIds, not UUIDs. */
|
|
6
|
+
const OBJECT_ID_HEX_RE = /^[0-9a-f]{24}$/;
|
|
7
|
+
export function isObjectIdHex(value) {
|
|
8
|
+
return OBJECT_ID_HEX_RE.test(value);
|
|
9
|
+
}
|
|
10
|
+
export function parseLinkFile(contents) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(contents);
|
|
13
|
+
const raw = typeof parsed.projectId === "string" ? parsed.projectId : null;
|
|
14
|
+
if (!raw)
|
|
15
|
+
return null;
|
|
16
|
+
const id = raw.trim().toLowerCase();
|
|
17
|
+
if (!isObjectIdHex(id))
|
|
18
|
+
return null;
|
|
19
|
+
const rawEnv = typeof parsed.environment === "string"
|
|
20
|
+
? parsed.environment.trim()
|
|
21
|
+
: typeof parsed.environment === "number"
|
|
22
|
+
? String(parsed.environment)
|
|
23
|
+
: "dev";
|
|
24
|
+
const environment = rawEnv.length ? rawEnv : "dev";
|
|
25
|
+
return { projectId: id, environment };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Write the canonical link file (`.envspot.json`) in `dir`. Returns its path. */
|
|
32
|
+
export function writeLinkConfig(dir, payload) {
|
|
33
|
+
const filePath = join(dir, LINK_PRIMARY);
|
|
34
|
+
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
35
|
+
return filePath;
|
|
36
|
+
}
|
|
37
|
+
/** Walk cwd → parents for `.envspot.json` / `.envspot` */
|
|
38
|
+
export function findEnvspotLink(startDir) {
|
|
39
|
+
let dir = resolve(startDir ?? process.cwd());
|
|
40
|
+
let sawUnparsable = false;
|
|
41
|
+
for (;;) {
|
|
42
|
+
for (const name of [LINK_PRIMARY, LINK_LEGACY]) {
|
|
43
|
+
const p = join(dir, name);
|
|
44
|
+
if (!existsSync(p))
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const txt = readFileSync(p, "utf8");
|
|
48
|
+
const link = parseLinkFile(txt);
|
|
49
|
+
if (link)
|
|
50
|
+
return { link, dir };
|
|
51
|
+
sawUnparsable = true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* try sibling name */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const parent = dirname(dir);
|
|
58
|
+
if (parent === dir)
|
|
59
|
+
break;
|
|
60
|
+
dir = parent;
|
|
61
|
+
}
|
|
62
|
+
// A link file exists but its projectId didn't parse (e.g. a pre-Mongo UUID).
|
|
63
|
+
// Nudge toward a relink rather than letting callers report a bare "not linked".
|
|
64
|
+
if (sawUnparsable) {
|
|
65
|
+
console.warn("Found an .envspot.json but its projectId isn't a valid project id (old format?). Run: envspot link");
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
const MANIFESTS = [
|
|
4
|
+
"package.json",
|
|
5
|
+
"pyproject.toml",
|
|
6
|
+
"go.mod",
|
|
7
|
+
"Cargo.toml",
|
|
8
|
+
"fly.toml",
|
|
9
|
+
"Dockerfile",
|
|
10
|
+
".git",
|
|
11
|
+
];
|
|
12
|
+
/** Nearest ancestor (within `maxUp`) that looks like a project root, else null. */
|
|
13
|
+
export function findProjectRoot(startDir, maxUp = 3) {
|
|
14
|
+
let dir = resolve(startDir);
|
|
15
|
+
for (let up = 0; up <= maxUp; up++) {
|
|
16
|
+
if (MANIFESTS.some((m) => existsSync(join(dir, m))))
|
|
17
|
+
return dir;
|
|
18
|
+
const parent = dirname(dir);
|
|
19
|
+
if (parent === dir)
|
|
20
|
+
break;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/** `package.json` paths (relative to `root`) within `maxDepth`, skipping node_modules/.git. */
|
|
26
|
+
export function findPackageJsons(root, maxDepth = 3) {
|
|
27
|
+
const found = [];
|
|
28
|
+
const walk = (dir, depth) => {
|
|
29
|
+
if (depth > maxDepth)
|
|
30
|
+
return;
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (e.name === "node_modules" || e.name === ".git")
|
|
40
|
+
continue;
|
|
41
|
+
const full = join(dir, e.name);
|
|
42
|
+
if (e.isFile() && e.name === "package.json") {
|
|
43
|
+
found.push(relative(root, full) || "package.json");
|
|
44
|
+
}
|
|
45
|
+
else if (e.isDirectory()) {
|
|
46
|
+
walk(full, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(root, 0);
|
|
51
|
+
return found.sort();
|
|
52
|
+
}
|
|
53
|
+
function slugify(s) {
|
|
54
|
+
return s
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
57
|
+
.replace(/^-+|-+$/g, "");
|
|
58
|
+
}
|
|
59
|
+
function readPackageName(root) {
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
62
|
+
return typeof pkg.name === "string" && pkg.name.trim() ? pkg.name : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Suggested project display name + slug from package.json#name or the dir name. */
|
|
69
|
+
export function deriveProjectName(root) {
|
|
70
|
+
const pkgName = readPackageName(root);
|
|
71
|
+
if (pkgName) {
|
|
72
|
+
if (pkgName.startsWith("@")) {
|
|
73
|
+
const [scope, rest] = pkgName.slice(1).split("/");
|
|
74
|
+
const name = rest || scope || "";
|
|
75
|
+
const slug = slugify(`${scope}-${rest ?? ""}`);
|
|
76
|
+
if (name && slug)
|
|
77
|
+
return { name, slug };
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const slug = slugify(pkgName);
|
|
81
|
+
if (slug)
|
|
82
|
+
return { name: pkgName, slug };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// No usable package.json name (or it slugified to empty) → fall back to the dir.
|
|
86
|
+
const dirName = basename(resolve(root));
|
|
87
|
+
return { name: dirName || "project", slug: slugify(dirName) || "project" };
|
|
88
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants as osConstants } from "node:os";
|
|
4
|
+
import { getStoredToken } from "./auth-store.js";
|
|
5
|
+
import { authRequired } from "./errors.js";
|
|
6
|
+
import { fetchProjectSecrets, resolveLinkScope } from "./secrets.js";
|
|
7
|
+
import { isHeadlessToken } from "./token-kind.js";
|
|
8
|
+
/** Arguments after `envspot run` (i.e. `process.argv.slice(3)` when argv[2]==='run'). */
|
|
9
|
+
export function parseRunTailArgs(parts) {
|
|
10
|
+
let env;
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < parts.length) {
|
|
13
|
+
const a = parts[i];
|
|
14
|
+
if (a === "--") {
|
|
15
|
+
const child = parts.slice(i + 1);
|
|
16
|
+
if (child.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
message: "Missing command after `--`. Example: envspot run -- npm start",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, env, child };
|
|
23
|
+
}
|
|
24
|
+
if (a === "-e" || a === "--env") {
|
|
25
|
+
const v = parts[i + 1];
|
|
26
|
+
if (!v?.trim())
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
message: "`--env` requires a non-empty environment name.",
|
|
30
|
+
};
|
|
31
|
+
env = v.trim();
|
|
32
|
+
i += 2;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (a.startsWith("--env=")) {
|
|
36
|
+
const v = a.slice("--env=".length).trim();
|
|
37
|
+
if (!v)
|
|
38
|
+
return { ok: false, message: "`--env=` requires a non-empty value." };
|
|
39
|
+
env = v;
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
message: `Unexpected token before '--': "${a}". Use: envspot run [--env <name>] -- <command>`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: "Missing `--` before the wrapped command (e.g. envspot run -- npm start).",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Spawn the wrapped command with secrets injected; resolves with its exit code. */
|
|
54
|
+
function spawnWithSecrets(command, args, secrets) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const childProc = spawn(command, args, {
|
|
57
|
+
env: { ...process.env, ...secrets },
|
|
58
|
+
stdio: "inherit",
|
|
59
|
+
shell: false,
|
|
60
|
+
});
|
|
61
|
+
childProc.on("error", (err) => {
|
|
62
|
+
if (err.code === "ENOENT") {
|
|
63
|
+
const b = basename(command.replace(/\\/g, "/"));
|
|
64
|
+
console.error(`Couldn't run \`${b}\`: command not found.\nIs it installed and on your PATH?`);
|
|
65
|
+
resolve(127);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error(`Failed to spawn child: ${err.message}`);
|
|
69
|
+
resolve(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
childProc.on("close", (c, signal) => {
|
|
73
|
+
if (signal) {
|
|
74
|
+
const signum = osConstants.signals[signal] ?? 0;
|
|
75
|
+
resolve(signum ? 128 + signum : (c ?? 1));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
resolve(c ?? 0);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Inject the linked project's secrets and exec the wrapped command; resolves with its exit code. */
|
|
84
|
+
export async function executeRun(parts) {
|
|
85
|
+
const token = await getStoredToken();
|
|
86
|
+
if (!token)
|
|
87
|
+
throw authRequired();
|
|
88
|
+
// A headless token self-scopes to one project + env, so CI needs no link
|
|
89
|
+
// file; a session bearer reads the linked project.
|
|
90
|
+
const secrets = isHeadlessToken(token)
|
|
91
|
+
? await fetchProjectSecrets(token)
|
|
92
|
+
: await fetchProjectSecrets(token, resolveLinkScope(parts.env));
|
|
93
|
+
return spawnWithSecrets(parts.child[0], parts.child.slice(1), secrets);
|
|
94
|
+
}
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { cliApiError } from "./cli-api-error.js";
|
|
2
|
+
import { notLinked } from "./errors.js";
|
|
3
|
+
import { apiUrl, backoffTransport, fetchWithCliHeaders, parseJsonSafely, rethrowTransport, } from "./http.js";
|
|
4
|
+
import { findEnvspotLink } from "./paths.js";
|
|
5
|
+
import { isHeadlessToken } from "./token-kind.js";
|
|
6
|
+
/** Resolve the session-bearer scope from the nearest `.envspot.json`, applying
|
|
7
|
+
* an optional env override. Throws `notLinked` when no link file is found. */
|
|
8
|
+
export function resolveLinkScope(envOverride) {
|
|
9
|
+
const linked = findEnvspotLink(process.cwd());
|
|
10
|
+
if (!linked)
|
|
11
|
+
throw notLinked();
|
|
12
|
+
return {
|
|
13
|
+
projectId: linked.link.projectId,
|
|
14
|
+
environment: envOverride?.trim() || linked.link.environment,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fetch a project's decrypted secrets as a `{ KEY: value }` map. Routes by
|
|
19
|
+
* token kind: a headless token self-scopes via /api/v1/secrets (CI; `scope` is
|
|
20
|
+
* ignored), a device-login session bearer reads the linked project + env via
|
|
21
|
+
* /api/cli/secrets. Throws a typed `CliError` on failure.
|
|
22
|
+
*/
|
|
23
|
+
export async function fetchProjectSecrets(token, scope) {
|
|
24
|
+
const url = isHeadlessToken(token)
|
|
25
|
+
? apiUrl("/api/v1/secrets")
|
|
26
|
+
: `${apiUrl("/api/cli/secrets")}?${new URLSearchParams({
|
|
27
|
+
project: scope?.projectId ?? "",
|
|
28
|
+
env: scope?.environment ?? "",
|
|
29
|
+
}).toString()}`;
|
|
30
|
+
let res;
|
|
31
|
+
try {
|
|
32
|
+
res = await backoffTransport(async () => fetchWithCliHeaders(url, { method: "GET" }, token));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
rethrowTransport(e);
|
|
36
|
+
}
|
|
37
|
+
const analyzed = await parseJsonSafely(res);
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
throw await cliApiError(res.status, analyzed.json, scope?.projectId);
|
|
40
|
+
}
|
|
41
|
+
const out = {};
|
|
42
|
+
const secrets = analyzed.json?.secrets;
|
|
43
|
+
if (secrets && typeof secrets === "object") {
|
|
44
|
+
for (const [k, v] of Object.entries(secrets)) {
|
|
45
|
+
if (typeof v === "string")
|
|
46
|
+
out[k] = v;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI bearers come in two kinds, distinguished by mint prefix:
|
|
3
|
+
* - `esc_` device-login session bearer (org-wide; uses /api/cli/*)
|
|
4
|
+
* - `esh_` headless token (project+env scoped, read-only; uses /api/v1/secrets)
|
|
5
|
+
* The CLI routes secret reads to the matching endpoint by kind.
|
|
6
|
+
*/
|
|
7
|
+
export function isHeadlessToken(token) {
|
|
8
|
+
return token.startsWith("esh_");
|
|
9
|
+
}
|
|
10
|
+
/** Endpoint that validates a token of this kind — used to probe auth without
|
|
11
|
+
* false-failing on the wrong-kind endpoint (status check, token login). */
|
|
12
|
+
export function probePathForToken(token) {
|
|
13
|
+
return isHeadlessToken(token) ? "/api/v1/secrets" : "/api/cli/projects";
|
|
14
|
+
}
|
package/dist/whoami.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getStoredToken } from "./auth-store.js";
|
|
2
|
+
import { cliApiError } from "./cli-api-error.js";
|
|
3
|
+
import { CliError, ERR, notLoggedIn } from "./errors.js";
|
|
4
|
+
import { apiUrl, backoffTransport, fetchWithCliHeaders, parseJsonSafely, rethrowTransport, } from "./http.js";
|
|
5
|
+
import { findEnvspotLink } from "./paths.js";
|
|
6
|
+
import { isHeadlessToken } from "./token-kind.js";
|
|
7
|
+
/** Bearer signing tolerates this much clock skew; past it, auth gets flaky. */
|
|
8
|
+
const CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1000;
|
|
9
|
+
/** Mask a stored token for display: kind prefix + a few chars, rest hidden.
|
|
10
|
+
* The server never sees plaintext, so the prefix is computed here. */
|
|
11
|
+
export function maskToken(token) {
|
|
12
|
+
// A short/corrupt credential must not print in full behind a fake mask.
|
|
13
|
+
if (token.length <= 10)
|
|
14
|
+
return "••••";
|
|
15
|
+
return `${token.slice(0, 10)}••••`;
|
|
16
|
+
}
|
|
17
|
+
/** E0074 warning when the server's `Date` header drifts past tolerance from the
|
|
18
|
+
* local clock, or null when in range / the header is missing or unparseable. */
|
|
19
|
+
export function clockSkewWarning(serverDateHeader, nowMs) {
|
|
20
|
+
if (!serverDateHeader)
|
|
21
|
+
return null;
|
|
22
|
+
const serverMs = Date.parse(serverDateHeader);
|
|
23
|
+
if (Number.isNaN(serverMs))
|
|
24
|
+
return null;
|
|
25
|
+
const skewMs = nowMs - serverMs;
|
|
26
|
+
if (Math.abs(skewMs) <= CLOCK_SKEW_TOLERANCE_MS)
|
|
27
|
+
return null;
|
|
28
|
+
const minutes = Math.round(Math.abs(skewMs) / 60_000);
|
|
29
|
+
return (`warning[E0074]: System clock differs from envspot.com by ${minutes}m ` +
|
|
30
|
+
`(local: ${new Date(nowMs).toISOString()}, server: ${new Date(serverMs).toISOString()}).\n` +
|
|
31
|
+
` help: Bearer signing tolerates 5 minutes of skew; past that, expect intermittent auth failures. Ensure NTP is running.\n` +
|
|
32
|
+
` docs: https://docs.envspot.com/errors/E0074`);
|
|
33
|
+
}
|
|
34
|
+
function titleCaseTier(tier) {
|
|
35
|
+
return tier.length ? tier[0].toUpperCase() + tier.slice(1) : tier;
|
|
36
|
+
}
|
|
37
|
+
/** Render the human-readable whoami block. Pure: the caller resolves the local
|
|
38
|
+
* link so this never touches the filesystem. */
|
|
39
|
+
export function formatWhoami(data, token, link) {
|
|
40
|
+
const named = data.user.name ? ` (${data.user.name})` : "";
|
|
41
|
+
const lines = [
|
|
42
|
+
`Signed in as ${data.user.email}${named}`,
|
|
43
|
+
`Workspace: ${data.org.slug} (${titleCaseTier(data.org.tier)})`,
|
|
44
|
+
];
|
|
45
|
+
if (isHeadlessToken(token)) {
|
|
46
|
+
// A headless token carries its own project + env + scope binding; that is
|
|
47
|
+
// the authoritative "where am I pointed", so it lives on the Token line.
|
|
48
|
+
lines.push(`Token: ${maskToken(token)} (project: ${data.project?.slug ?? "unknown"} · env: ${data.project?.env ?? "?"} · scope: ${data.scope ?? "?"})`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
lines.push(`Active link: ${link ? `${link.projectId} · ${link.environment} (from ./.envspot.json)` : "none (run `envspot link`)"}`);
|
|
52
|
+
lines.push(`Token: ${maskToken(token)} (CLI session, no scope restriction)`);
|
|
53
|
+
}
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
/** `envspot whoami` — print the bound user + workspace (and, for a headless
|
|
57
|
+
* token, its project/env/scope). `nowMs` is injectable for deterministic
|
|
58
|
+
* clock-skew tests. */
|
|
59
|
+
export async function executeWhoami(opts, nowMs = Date.now()) {
|
|
60
|
+
const token = await getStoredToken();
|
|
61
|
+
if (!token)
|
|
62
|
+
throw notLoggedIn();
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await backoffTransport(async () => fetchWithCliHeaders(apiUrl("/api/cli/whoami"), { method: "GET" }, token));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
rethrowTransport(e);
|
|
69
|
+
}
|
|
70
|
+
const analyzed = await parseJsonSafely(res);
|
|
71
|
+
if (!res.ok)
|
|
72
|
+
throw await cliApiError(res.status, analyzed.json);
|
|
73
|
+
// A 200 with an empty or non-JSON body (proxy page, truncated response) would
|
|
74
|
+
// otherwise null-deref in formatWhoami or print "null" under --json.
|
|
75
|
+
const data = analyzed.json;
|
|
76
|
+
if (!data || typeof data.user !== "object" || data.user === null) {
|
|
77
|
+
throw new CliError(ERR.UNEXPECTED, "Malformed response from envspot (expected whoami JSON).");
|
|
78
|
+
}
|
|
79
|
+
// Emit the skew warning before the --json early return: CI uses --json, and
|
|
80
|
+
// that is exactly where clock skew silently breaks bearer auth. It goes to
|
|
81
|
+
// stderr, so it never pollutes the JSON on stdout.
|
|
82
|
+
const warning = clockSkewWarning(res.headers.get("date"), nowMs);
|
|
83
|
+
if (warning)
|
|
84
|
+
console.error(warning);
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const link = isHeadlessToken(token)
|
|
90
|
+
? null
|
|
91
|
+
: (findEnvspotLink(process.cwd())?.link ?? null);
|
|
92
|
+
console.log(formatWhoami(data, token, link));
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "envspot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for envspot — encrypted environment variables for your team",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "EnvSpot",
|
|
7
|
+
"homepage": "https://envspot.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/EnvSpot/envspot.git",
|
|
11
|
+
"directory": "cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/EnvSpot/envspot/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"envspot",
|
|
18
|
+
"env",
|
|
19
|
+
"environment-variables",
|
|
20
|
+
"secrets",
|
|
21
|
+
"secrets-management",
|
|
22
|
+
"dotenv",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"envspot": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
40
|
+
"dev": "tsx src/index.ts",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "^12.0.0",
|
|
45
|
+
"keytar": "^7.9.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"tsx": "^4.21.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|