envpass-cli 0.0.0-stage → 0.1.6
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 +44 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +183 -0
- package/dist/client.d.ts +49 -0
- package/dist/client.js +81 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +87 -0
- package/dist/device.d.ts +13 -0
- package/dist/device.js +56 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mask.d.ts +3 -0
- package/dist/mask.js +59 -0
- package/dist/schema.d.ts +5440 -0
- package/dist/schema.js +5 -0
- package/package.json +49 -4
package/README.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# envpass-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Client library and CLI for [envpass](https://envpass.dev): encrypted environment variables for every project, service and environment.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx envpass-cli login # sign in through your browser
|
|
9
|
+
bunx envpass-cli link -w acme -p shop -e dev # save defaults to .envpass.json (no secrets inside)
|
|
10
|
+
bunx envpass-cli pull # write .env
|
|
11
|
+
bunx envpass-cli pull -e prod -o - # print to stdout
|
|
12
|
+
bunx envpass-cli run -e dev -- bun dev # run with values injected, masked in output
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`npx envpass-cli …` works the same way.
|
|
16
|
+
|
|
17
|
+
In CI, use a service token instead of `login`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ENVPASS_TOKEN=ep_live_… npx envpass-cli pull -e prod
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Option | Environment variable | |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `-w, --workspace` | `ENVPASS_WORKSPACE` | Workspace slug (not needed with a service token) |
|
|
26
|
+
| `-p, --project` | `ENVPASS_PROJECT` | Project slug (not needed with a service token) |
|
|
27
|
+
| `-e, --env` | `ENVPASS_ENV` | Environment slug |
|
|
28
|
+
| `-s, --service` | `ENVPASS_SERVICE` | Service slug, or `shared` |
|
|
29
|
+
| `--url` | `ENVPASS_URL` | Server, default `https://envpass.dev` |
|
|
30
|
+
| | `ENVPASS_TOKEN` | Service token or CLI session token |
|
|
31
|
+
|
|
32
|
+
## Library
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createEnvpass, loadEnv } from "envpass-cli"
|
|
36
|
+
|
|
37
|
+
await loadEnv({ environment: "prod" })
|
|
38
|
+
|
|
39
|
+
const envpass = createEnvpass({ token: process.env.ENVPASS_TOKEN })
|
|
40
|
+
const entries = await envpass.resolve({ environment: "prod", service: "api" })
|
|
41
|
+
|
|
42
|
+
const { data } = await envpass.api.GET("/api/v1/workspaces")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`envpass.api` is a fully typed client for the whole REST API, generated from the OpenAPI spec.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { createEnvpass, EnvpassError } from "./client.js";
|
|
7
|
+
import { baseUrl, forgetToken, resolveScope, saveToken, storedToken, writeProjectLink, } from "./config.js";
|
|
8
|
+
import { deviceLogin, openBrowser } from "./device.js";
|
|
9
|
+
import { maskingStream } from "./mask.js";
|
|
10
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
11
|
+
const HELP = `envpass ${version}: encrypted environment variables for every project, service and environment
|
|
12
|
+
|
|
13
|
+
Usage
|
|
14
|
+
envpass login Sign in through your browser
|
|
15
|
+
envpass logout Forget the saved login
|
|
16
|
+
envpass whoami Show who the current token belongs to
|
|
17
|
+
envpass link -w <ws> -p <project> Save defaults to .envpass.json in this directory
|
|
18
|
+
envpass pull [-e env] [-o .env] Write resolved values to a file ("-" for stdout)
|
|
19
|
+
envpass run [-e env] -- <command> Run a command with the values injected
|
|
20
|
+
|
|
21
|
+
Options
|
|
22
|
+
-w, --workspace <slug> Workspace (not needed with a service token)
|
|
23
|
+
-p, --project <slug> Project (not needed with a service token)
|
|
24
|
+
-e, --env <slug> Environment, e.g. dev or prod
|
|
25
|
+
-s, --service <slug> Service layer, or "shared"
|
|
26
|
+
-o, --out <file> pull: output file (default .env)
|
|
27
|
+
-f, --format <format> pull: dotenv, shell or json (default dotenv)
|
|
28
|
+
--no-mask run: show secret values in the command's output
|
|
29
|
+
--no-browser login: print the link instead of opening a browser
|
|
30
|
+
--url <url> envpass server (default https://envpass.dev, or ENVPASS_URL)
|
|
31
|
+
|
|
32
|
+
Environment
|
|
33
|
+
ENVPASS_TOKEN A service token (ep_live_…) or CLI session token
|
|
34
|
+
ENVPASS_WORKSPACE, ENVPASS_PROJECT, ENVPASS_ENV, ENVPASS_SERVICE
|
|
35
|
+
`;
|
|
36
|
+
function fail(message, code = 1) {
|
|
37
|
+
process.stderr.write(`envpass: ${message}\n`);
|
|
38
|
+
process.exit(code);
|
|
39
|
+
}
|
|
40
|
+
async function main(argv) {
|
|
41
|
+
const { values, positionals } = parseArgs({
|
|
42
|
+
args: argv,
|
|
43
|
+
allowPositionals: true,
|
|
44
|
+
options: {
|
|
45
|
+
workspace: { type: "string", short: "w" },
|
|
46
|
+
project: { type: "string", short: "p" },
|
|
47
|
+
env: { type: "string", short: "e" },
|
|
48
|
+
service: { type: "string", short: "s" },
|
|
49
|
+
out: { type: "string", short: "o" },
|
|
50
|
+
format: { type: "string", short: "f" },
|
|
51
|
+
url: { type: "string" },
|
|
52
|
+
"no-mask": { type: "boolean" },
|
|
53
|
+
"no-browser": { type: "boolean" },
|
|
54
|
+
help: { type: "boolean", short: "h" },
|
|
55
|
+
version: { type: "boolean", short: "v" },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const [command, ...rest] = positionals;
|
|
59
|
+
if (values.version)
|
|
60
|
+
return void process.stdout.write(`${version}\n`);
|
|
61
|
+
if (values.help || !command || command === "help")
|
|
62
|
+
return void process.stdout.write(HELP);
|
|
63
|
+
const url = baseUrl(values.url);
|
|
64
|
+
const flags = {
|
|
65
|
+
workspace: values.workspace,
|
|
66
|
+
project: values.project,
|
|
67
|
+
environment: values.env,
|
|
68
|
+
service: values.service,
|
|
69
|
+
};
|
|
70
|
+
switch (command) {
|
|
71
|
+
case "login": {
|
|
72
|
+
const token = await deviceLogin(url, (code) => {
|
|
73
|
+
const link = code.verification_uri_complete ?? code.verification_uri;
|
|
74
|
+
process.stderr.write(`\nOpen ${link}\nand confirm the code: ${code.user_code}\n\nWaiting for approval…\n`);
|
|
75
|
+
if (!values["no-browser"])
|
|
76
|
+
openBrowser(link);
|
|
77
|
+
});
|
|
78
|
+
const path = saveToken(url, token);
|
|
79
|
+
const me = await createEnvpass({ baseUrl: url, token }).me();
|
|
80
|
+
process.stderr.write(`Signed in. Token saved to ${path}\n`);
|
|
81
|
+
if (me.workspaces.length) {
|
|
82
|
+
process.stderr.write(`Workspaces: ${me.workspaces.map((w) => w.slug).join(", ")}\n`);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
case "logout": {
|
|
87
|
+
const token = storedToken(url);
|
|
88
|
+
if (token) {
|
|
89
|
+
await fetch(`${url}/api/auth/sign-out`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
92
|
+
body: "{}",
|
|
93
|
+
}).catch(() => undefined);
|
|
94
|
+
}
|
|
95
|
+
const removed = forgetToken(url);
|
|
96
|
+
process.stderr.write(removed ? `Signed out of ${url}\n` : `No saved login for ${url}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
case "whoami": {
|
|
100
|
+
const client = createEnvpass({ baseUrl: url });
|
|
101
|
+
if (!client.hasToken)
|
|
102
|
+
fail("Not signed in. Run `envpass login` or set ENVPASS_TOKEN.");
|
|
103
|
+
const me = await client.me();
|
|
104
|
+
if (me.token) {
|
|
105
|
+
process.stdout.write(`Service token ${me.token.id} (${me.token.access}) for project ${me.token.projectId}\n`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
process.stdout.write(`User ${me.userId}\n`);
|
|
109
|
+
for (const ws of me.workspaces)
|
|
110
|
+
process.stdout.write(` ${ws.slug}${ws.isOwner ? " (owner)" : ""}\n`);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case "link": {
|
|
115
|
+
const scope = resolveScope(flags);
|
|
116
|
+
if (!scope.workspace || !scope.project)
|
|
117
|
+
fail("Pass --workspace and --project.");
|
|
118
|
+
const path = writeProjectLink(scope);
|
|
119
|
+
process.stderr.write(`Saved ${path}. It holds no secrets and is safe to commit.\n`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
case "pull": {
|
|
123
|
+
const format = (values.format ?? "dotenv");
|
|
124
|
+
if (!["dotenv", "shell", "json"].includes(format))
|
|
125
|
+
fail("--format must be dotenv, shell or json");
|
|
126
|
+
const client = createEnvpass({ baseUrl: url });
|
|
127
|
+
if (!client.hasToken)
|
|
128
|
+
fail("Not signed in. Run `envpass login` or set ENVPASS_TOKEN.");
|
|
129
|
+
const text = format === "json"
|
|
130
|
+
? `${JSON.stringify(Object.fromEntries((await client.resolve(flags)).map((e) => [e.key, e.value])), null, 2)}\n`
|
|
131
|
+
: await client.resolveText(flags, format);
|
|
132
|
+
const out = values.out ?? ".env";
|
|
133
|
+
if (out === "-")
|
|
134
|
+
return void process.stdout.write(text);
|
|
135
|
+
writeFileSync(out, text, { mode: 0o600 });
|
|
136
|
+
const count = text
|
|
137
|
+
.split("\n")
|
|
138
|
+
.filter((line) => line.includes("=") || line.includes(":")).length;
|
|
139
|
+
process.stderr.write(`Wrote ${out} (${count} values). Keep it out of git.\n`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case "run": {
|
|
143
|
+
const separator = argv.indexOf("--");
|
|
144
|
+
const commandArgs = separator === -1 ? rest : argv.slice(separator + 1);
|
|
145
|
+
const [program, ...programArgs] = commandArgs;
|
|
146
|
+
if (!program)
|
|
147
|
+
fail("Usage: envpass run [-e env] -- <command> [args…]");
|
|
148
|
+
const client = createEnvpass({ baseUrl: url });
|
|
149
|
+
if (!client.hasToken)
|
|
150
|
+
fail("Not signed in. Run `envpass login` or set ENVPASS_TOKEN.");
|
|
151
|
+
const entries = await client.resolve(flags);
|
|
152
|
+
const env = { ...process.env, ...Object.fromEntries(entries.map((e) => [e.key, e.value])) };
|
|
153
|
+
const mask = !values["no-mask"];
|
|
154
|
+
const child = spawn(program, programArgs, {
|
|
155
|
+
env,
|
|
156
|
+
stdio: mask ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
157
|
+
});
|
|
158
|
+
if (mask) {
|
|
159
|
+
const secrets = entries.map((e) => e.value);
|
|
160
|
+
child.stdout?.pipe(maskingStream(secrets)).pipe(process.stdout);
|
|
161
|
+
child.stderr?.pipe(maskingStream(secrets)).pipe(process.stderr);
|
|
162
|
+
}
|
|
163
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
164
|
+
process.on(signal, () => child.kill(signal));
|
|
165
|
+
}
|
|
166
|
+
child.on("error", (error) => fail(`Could not start ${program}: ${error.message}`, 127));
|
|
167
|
+
child.on("exit", (code, signal) => {
|
|
168
|
+
if (signal)
|
|
169
|
+
process.kill(process.pid, signal);
|
|
170
|
+
else
|
|
171
|
+
process.exit(code ?? 0);
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
fail(`Unknown command "${command}". Run \`envpass --help\`.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
180
|
+
if (error instanceof EnvpassError)
|
|
181
|
+
fail(`${error.message}${error.code ? ` (${error.code})` : ""}`);
|
|
182
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
183
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ProjectLink } from "./config.js";
|
|
2
|
+
import type { components, paths } from "./schema.js";
|
|
3
|
+
export type Source = components["schemas"]["SecretSource"];
|
|
4
|
+
export type ResolvedEntry = {
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
source: Source;
|
|
8
|
+
};
|
|
9
|
+
export type ExportFormat = "dotenv" | "json" | "shell";
|
|
10
|
+
export type EnvpassOptions = {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
token?: string;
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class EnvpassError extends Error {
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly code?: string | undefined;
|
|
18
|
+
constructor(message: string, status: number, code?: string | undefined);
|
|
19
|
+
}
|
|
20
|
+
export declare function createEnvpass(options?: EnvpassOptions): {
|
|
21
|
+
url: string;
|
|
22
|
+
hasToken: boolean;
|
|
23
|
+
api: import("openapi-fetch").Client<paths, `${string}/${string}`>;
|
|
24
|
+
me(): Promise<{
|
|
25
|
+
actor: "user" | "token";
|
|
26
|
+
userId: string;
|
|
27
|
+
token: {
|
|
28
|
+
id: string;
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
projectId: string;
|
|
31
|
+
serviceId: string | null;
|
|
32
|
+
access: "read" | "write";
|
|
33
|
+
} | null;
|
|
34
|
+
workspaces: {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
isOwner: boolean;
|
|
39
|
+
}[];
|
|
40
|
+
}>;
|
|
41
|
+
resolve(scope?: ProjectLink): Promise<ResolvedEntry[]>;
|
|
42
|
+
resolveText(scope?: ProjectLink, format?: Exclude<ExportFormat, "json">): Promise<string>;
|
|
43
|
+
};
|
|
44
|
+
export type Envpass = ReturnType<typeof createEnvpass>;
|
|
45
|
+
export declare function loadEnv(scope?: ProjectLink & EnvpassOptions & {
|
|
46
|
+
override?: boolean;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
[k: string]: string;
|
|
49
|
+
}>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import createFetchClient from "openapi-fetch";
|
|
2
|
+
import { baseUrl, resolveScope, resolveToken } from "./config.js";
|
|
3
|
+
export class EnvpassError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, status, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "EnvpassError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function errorFrom(status, body) {
|
|
14
|
+
const error = body?.error;
|
|
15
|
+
return new EnvpassError(error?.message ?? `Request failed with status ${status}`, status, error?.code);
|
|
16
|
+
}
|
|
17
|
+
export function createEnvpass(options = {}) {
|
|
18
|
+
const url = baseUrl(options.baseUrl);
|
|
19
|
+
const token = resolveToken(url, options.token);
|
|
20
|
+
const headers = { "user-agent": options.userAgent ?? "envpass-node" };
|
|
21
|
+
if (token)
|
|
22
|
+
headers.authorization = `Bearer ${token}`;
|
|
23
|
+
const api = createFetchClient({ baseUrl: url, headers });
|
|
24
|
+
function scopeQuery(scope) {
|
|
25
|
+
if (!scope.environment) {
|
|
26
|
+
throw new EnvpassError("No environment. Pass --env, set ENVPASS_ENV, or run `envpass link`.", 400, "invalid");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
workspace: scope.workspace,
|
|
30
|
+
project: scope.project,
|
|
31
|
+
environment: scope.environment,
|
|
32
|
+
service: scope.service,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
url,
|
|
37
|
+
hasToken: Boolean(token),
|
|
38
|
+
api,
|
|
39
|
+
async me() {
|
|
40
|
+
const { data, error, response } = await api.GET("/api/v1/me");
|
|
41
|
+
if (!data)
|
|
42
|
+
throw errorFrom(response.status, error);
|
|
43
|
+
return data;
|
|
44
|
+
},
|
|
45
|
+
async resolve(scope = {}) {
|
|
46
|
+
const query = scopeQuery(resolveScope(scope));
|
|
47
|
+
const { data, error, response } = await api.GET("/api/v1/resolve", {
|
|
48
|
+
params: { query: { ...query, format: "json" } },
|
|
49
|
+
});
|
|
50
|
+
if (!data || typeof data === "string")
|
|
51
|
+
throw errorFrom(response.status, error);
|
|
52
|
+
return data.entries;
|
|
53
|
+
},
|
|
54
|
+
async resolveText(scope = {}, format = "dotenv") {
|
|
55
|
+
const query = scopeQuery(resolveScope(scope));
|
|
56
|
+
const response = await fetch(`${url}/api/v1/resolve?${new URLSearchParams(Object.entries({ ...query, format }).filter((entry) => Boolean(entry[1])))}`, { headers });
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
let body;
|
|
60
|
+
try {
|
|
61
|
+
body = JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
body = undefined;
|
|
65
|
+
}
|
|
66
|
+
throw errorFrom(response.status, body);
|
|
67
|
+
}
|
|
68
|
+
return text;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export async function loadEnv(scope = {}) {
|
|
73
|
+
const { baseUrl: url, token, userAgent, override = false, ...link } = scope;
|
|
74
|
+
const client = createEnvpass({ baseUrl: url, token, userAgent });
|
|
75
|
+
const entries = await client.resolve(link);
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (override || process.env[entry.key] === undefined)
|
|
78
|
+
process.env[entry.key] = entry.value;
|
|
79
|
+
}
|
|
80
|
+
return Object.fromEntries(entries.map((entry) => [entry.key, entry.value]));
|
|
81
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_URL = "https://envpass.dev";
|
|
2
|
+
export type ProjectLink = {
|
|
3
|
+
workspace?: string;
|
|
4
|
+
project?: string;
|
|
5
|
+
environment?: string;
|
|
6
|
+
service?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function baseUrl(explicit?: string): string;
|
|
9
|
+
export declare function saveToken(url: string, token: string): string;
|
|
10
|
+
export declare function forgetToken(url: string): boolean;
|
|
11
|
+
export declare function storedToken(url: string): string | undefined;
|
|
12
|
+
export declare function resolveToken(url: string, explicit?: string): string | undefined;
|
|
13
|
+
export declare function writeProjectLink(link: ProjectLink, dir?: string): string;
|
|
14
|
+
export declare function resolveScope(flags: ProjectLink, start?: string): ProjectLink;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
export const DEFAULT_URL = "https://envpass.dev";
|
|
5
|
+
const PROJECT_FILE = ".envpass.json";
|
|
6
|
+
export function baseUrl(explicit) {
|
|
7
|
+
return (explicit ?? process.env.ENVPASS_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
8
|
+
}
|
|
9
|
+
function credentialsPath() {
|
|
10
|
+
const root = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
11
|
+
return join(root, "envpass", "credentials.json");
|
|
12
|
+
}
|
|
13
|
+
function readCredentials() {
|
|
14
|
+
const path = credentialsPath();
|
|
15
|
+
if (!existsSync(path))
|
|
16
|
+
return {};
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function saveToken(url, token) {
|
|
25
|
+
const path = credentialsPath();
|
|
26
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
27
|
+
const credentials = readCredentials();
|
|
28
|
+
credentials[url] = { token, savedAt: new Date().toISOString() };
|
|
29
|
+
writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
|
|
30
|
+
chmodSync(path, 0o600);
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
export function forgetToken(url) {
|
|
34
|
+
const path = credentialsPath();
|
|
35
|
+
const credentials = readCredentials();
|
|
36
|
+
if (!credentials[url])
|
|
37
|
+
return false;
|
|
38
|
+
delete credentials[url];
|
|
39
|
+
if (Object.keys(credentials).length === 0)
|
|
40
|
+
rmSync(path, { force: true });
|
|
41
|
+
else
|
|
42
|
+
writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
export function storedToken(url) {
|
|
46
|
+
return readCredentials()[url]?.token;
|
|
47
|
+
}
|
|
48
|
+
export function resolveToken(url, explicit) {
|
|
49
|
+
return explicit ?? process.env.ENVPASS_TOKEN ?? storedToken(url);
|
|
50
|
+
}
|
|
51
|
+
function findProjectFile(start = process.cwd()) {
|
|
52
|
+
let dir = resolve(start);
|
|
53
|
+
for (;;) {
|
|
54
|
+
const candidate = join(dir, PROJECT_FILE);
|
|
55
|
+
if (existsSync(candidate))
|
|
56
|
+
return candidate;
|
|
57
|
+
const parent = dirname(dir);
|
|
58
|
+
if (parent === dir)
|
|
59
|
+
return undefined;
|
|
60
|
+
dir = parent;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function readProjectLink(start) {
|
|
64
|
+
const path = findProjectFile(start);
|
|
65
|
+
if (!path)
|
|
66
|
+
return {};
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`${path} is not valid JSON`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function writeProjectLink(link, dir = process.cwd()) {
|
|
75
|
+
const path = join(dir, PROJECT_FILE);
|
|
76
|
+
writeFileSync(path, `${JSON.stringify(link, null, 2)}\n`);
|
|
77
|
+
return path;
|
|
78
|
+
}
|
|
79
|
+
export function resolveScope(flags, start) {
|
|
80
|
+
const file = readProjectLink(start);
|
|
81
|
+
return {
|
|
82
|
+
workspace: flags.workspace ?? process.env.ENVPASS_WORKSPACE ?? file.workspace,
|
|
83
|
+
project: flags.project ?? process.env.ENVPASS_PROJECT ?? file.project,
|
|
84
|
+
environment: flags.environment ?? process.env.ENVPASS_ENV ?? file.environment,
|
|
85
|
+
service: flags.service ?? process.env.ENVPASS_SERVICE ?? file.service,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type DeviceCode = {
|
|
2
|
+
device_code: string;
|
|
3
|
+
user_code: string;
|
|
4
|
+
verification_uri: string;
|
|
5
|
+
verification_uri_complete?: string;
|
|
6
|
+
expires_in: number;
|
|
7
|
+
interval: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function openBrowser(url: string): void;
|
|
10
|
+
export declare function deviceLogin(url: string, onCode: (code: DeviceCode) => void, options?: {
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
export {};
|
package/dist/device.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const CLI_CLIENT_ID = "envpass-cli";
|
|
3
|
+
export function openBrowser(url) {
|
|
4
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
5
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
6
|
+
try {
|
|
7
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
8
|
+
child.on("error", () => { });
|
|
9
|
+
child.unref();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
export async function deviceLogin(url, onCode, options = {}) {
|
|
17
|
+
const http = options.fetch ?? fetch;
|
|
18
|
+
const start = await http(`${url}/api/auth/device/code`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ client_id: CLI_CLIENT_ID }),
|
|
22
|
+
});
|
|
23
|
+
if (!start.ok)
|
|
24
|
+
throw new Error(`Could not start login (${start.status})`);
|
|
25
|
+
const code = (await start.json());
|
|
26
|
+
onCode(code);
|
|
27
|
+
let interval = Math.max(1, code.interval) * 1000;
|
|
28
|
+
const deadline = Date.now() + code.expires_in * 1000;
|
|
29
|
+
while (Date.now() < deadline) {
|
|
30
|
+
await sleep(interval);
|
|
31
|
+
const poll = await http(`${url}/api/auth/device/token`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
36
|
+
device_code: code.device_code,
|
|
37
|
+
client_id: CLI_CLIENT_ID,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
const body = (await poll.json());
|
|
41
|
+
if (body.access_token)
|
|
42
|
+
return body.access_token;
|
|
43
|
+
if (body.error === "authorization_pending")
|
|
44
|
+
continue;
|
|
45
|
+
if (body.error === "slow_down") {
|
|
46
|
+
interval += 5000;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (body.error === "access_denied")
|
|
50
|
+
throw new Error("Login was denied in the browser.");
|
|
51
|
+
if (body.error === "expired_token")
|
|
52
|
+
throw new Error("The login code expired. Run `envpass login` again.");
|
|
53
|
+
throw new Error(body.error_description ?? body.error ?? `Login failed (${poll.status})`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error("The login code expired. Run `envpass login` again.");
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createEnvpass, type Envpass, EnvpassError, type EnvpassOptions, type ExportFormat, loadEnv, type ResolvedEntry, type Source, } from "./client.js";
|
|
2
|
+
export { DEFAULT_URL, type ProjectLink } from "./config.js";
|
|
3
|
+
export { maskText } from "./mask.js";
|
|
4
|
+
export type { components, paths } from "./schema.js";
|
package/dist/index.js
ADDED
package/dist/mask.d.ts
ADDED
package/dist/mask.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
const MASK = "***";
|
|
3
|
+
const MIN_LENGTH = 4;
|
|
4
|
+
export function maskText(text, secrets) {
|
|
5
|
+
let out = text;
|
|
6
|
+
for (const secret of secrets) {
|
|
7
|
+
if (secret.length >= MIN_LENGTH)
|
|
8
|
+
out = out.split(secret).join(MASK);
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
function partialSuffix(text, secrets) {
|
|
13
|
+
let longest = 0;
|
|
14
|
+
for (const secret of secrets) {
|
|
15
|
+
const max = Math.min(secret.length - 1, text.length);
|
|
16
|
+
for (let k = max; k > longest; k--) {
|
|
17
|
+
if (text.endsWith(secret.slice(0, k))) {
|
|
18
|
+
longest = k;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return longest;
|
|
24
|
+
}
|
|
25
|
+
export function maskingStream(values) {
|
|
26
|
+
const secrets = [...new Set(values.filter((v) => v.length >= MIN_LENGTH))].sort((a, b) => b.length - a.length);
|
|
27
|
+
let pending = "";
|
|
28
|
+
let timer;
|
|
29
|
+
const stream = new Transform({
|
|
30
|
+
transform(chunk, _encoding, callback) {
|
|
31
|
+
if (timer)
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
pending = maskText(pending + chunk.toString("utf8"), secrets);
|
|
34
|
+
const keep = partialSuffix(pending, secrets);
|
|
35
|
+
const emit = pending.slice(0, pending.length - keep);
|
|
36
|
+
pending = pending.slice(emit.length);
|
|
37
|
+
if (emit)
|
|
38
|
+
this.push(emit);
|
|
39
|
+
if (pending) {
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
const rest = pending;
|
|
42
|
+
pending = "";
|
|
43
|
+
if (rest)
|
|
44
|
+
stream.push(rest);
|
|
45
|
+
}, 100);
|
|
46
|
+
}
|
|
47
|
+
callback();
|
|
48
|
+
},
|
|
49
|
+
flush(callback) {
|
|
50
|
+
if (timer)
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
if (pending)
|
|
53
|
+
this.push(pending);
|
|
54
|
+
pending = "";
|
|
55
|
+
callback();
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return stream;
|
|
59
|
+
}
|