envpass-cli 0.0.0-stage → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js ADDED
@@ -0,0 +1,86 @@
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
+ };
86
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export { type Connection, createEnvpass, type Envpass, EnvpassError, type EnvpassOptions, type Environment, type ExportFormat, loadEnv, type NewWebhook, type ResolvedEntry, type Source, type Sync, WEBHOOK_EVENTS, type Webhook, type WebhookDelivery, type WebhookEvent, type WebhookFormat, type WebhookTestResult, type Workspace, } 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
@@ -0,0 +1,3 @@
1
+ export { createEnvpass, EnvpassError, loadEnv, WEBHOOK_EVENTS, } from "./client.js";
2
+ export { DEFAULT_URL } from "./config.js";
3
+ export { maskText } from "./mask.js";
@@ -0,0 +1,21 @@
1
+ import { type Envpass } from "./client.js";
2
+ import type { ProjectLink } from "./config.js";
3
+ export type Io = {
4
+ out: (text: string) => void;
5
+ err: (text: string) => void;
6
+ };
7
+ export type IntegrationArgs = {
8
+ scope: ProjectLink;
9
+ args: string[];
10
+ json?: boolean;
11
+ url?: string;
12
+ events?: string[];
13
+ environments?: string[];
14
+ projects?: string[];
15
+ format?: string;
16
+ description?: string;
17
+ limit?: string;
18
+ };
19
+ export declare function table(header: string[], rows: string[][]): string;
20
+ export declare function webhooksCommand(client: Envpass, options: IntegrationArgs, io: Io): Promise<number>;
21
+ export declare function syncsCommand(client: Envpass, options: IntegrationArgs, io: Io): Promise<number>;
@@ -0,0 +1,181 @@
1
+ import { WEBHOOK_EVENTS, } from "./client.js";
2
+ const WEBHOOKS_USAGE = "Usage: envpass webhooks <list|add|remove|test|deliveries>";
3
+ const SYNCS_USAGE = "Usage: envpass syncs <list|run|pause|resume>";
4
+ export function table(header, rows) {
5
+ const all = [header, ...rows];
6
+ const widths = header.map((_, i) => Math.max(...all.map((row) => (row[i] ?? "").length)));
7
+ return all
8
+ .map((row) => row.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i] ?? 0))).join(" "))
9
+ .join("\n")
10
+ .concat("\n");
11
+ }
12
+ function oneLine(text) {
13
+ return text ? text.replace(/\s+/g, " ").trim() : "-";
14
+ }
15
+ function json(value) {
16
+ return `${JSON.stringify(value, null, 2)}\n`;
17
+ }
18
+ function requireId(args, usage) {
19
+ const [id] = args;
20
+ if (!id)
21
+ throw new Error(usage);
22
+ return id;
23
+ }
24
+ function describeTest(result) {
25
+ const status = result.responseStatus === null ? "no response" : `HTTP ${result.responseStatus}`;
26
+ const detail = result.error ? `: ${oneLine(result.error)}` : "";
27
+ return `${result.ok ? "Delivered" : "Failed"}: ${status} in ${result.durationMs}ms${detail}\n`;
28
+ }
29
+ function parseLimit(limit) {
30
+ if (limit === undefined)
31
+ return undefined;
32
+ const value = Number(limit);
33
+ if (!/^[0-9]+$/.test(limit) || value < 1 || value > 100)
34
+ throw new Error("--limit must be a whole number from 1 to 100");
35
+ return value;
36
+ }
37
+ function parseEvents(events) {
38
+ if (!events?.length)
39
+ return undefined;
40
+ return events.map((event) => {
41
+ const known = WEBHOOK_EVENTS.find((candidate) => candidate === event);
42
+ if (!known)
43
+ throw new Error(`--event must be one of ${WEBHOOK_EVENTS.join(", ")}`);
44
+ return known;
45
+ });
46
+ }
47
+ function parseFormat(format) {
48
+ if (format === undefined)
49
+ return undefined;
50
+ if (format !== "json" && format !== "slack")
51
+ throw new Error("--format must be json or slack");
52
+ return format;
53
+ }
54
+ export async function webhooksCommand(client, options, io) {
55
+ const [action, ...args] = options.args;
56
+ const scope = { workspace: options.scope.workspace };
57
+ switch (action) {
58
+ case "list": {
59
+ const [webhooks, environments] = await Promise.all([
60
+ client.listWebhooks(scope),
61
+ client.listEnvironments(scope),
62
+ ]);
63
+ if (options.json) {
64
+ io.out(json(webhooks));
65
+ return 0;
66
+ }
67
+ if (!webhooks.length) {
68
+ io.err("No webhooks.\n");
69
+ return 0;
70
+ }
71
+ const slugs = new Map(environments.map((e) => [e.id, e.slug]));
72
+ io.out(table(["ID", "URL", "EVENTS", "ENVIRONMENTS", "ENABLED", "LAST DELIVERY"], webhooks.map((hook) => [
73
+ hook.id,
74
+ hook.url,
75
+ hook.events.join(","),
76
+ hook.environmentIds.length
77
+ ? hook.environmentIds.map((id) => slugs.get(id) ?? id).join(",")
78
+ : "all",
79
+ hook.enabled ? "on" : "off",
80
+ hook.lastDelivery
81
+ ? [hook.lastDelivery.status, hook.lastDelivery.responseStatus ?? ""].join(" ").trim()
82
+ : "-",
83
+ ])));
84
+ return 0;
85
+ }
86
+ case "add": {
87
+ if (!options.url)
88
+ throw new Error("Usage: envpass webhooks add --url <url> [options]");
89
+ const { webhook, secret } = await client.createWebhook({
90
+ url: options.url,
91
+ events: parseEvents(options.events),
92
+ environments: options.environments,
93
+ projects: options.projects,
94
+ format: parseFormat(options.format),
95
+ description: options.description,
96
+ }, scope);
97
+ io.out(`Webhook ${webhook.id} created for ${webhook.url}\n`);
98
+ io.out(`Signing secret: ${secret}\n`);
99
+ io.err("Store the signing secret now: it is shown only once. Use it to verify the envpass-signature header.\n");
100
+ return 0;
101
+ }
102
+ case "remove": {
103
+ const id = requireId(args, "Usage: envpass webhooks remove <id>");
104
+ await client.deleteWebhook(id, scope);
105
+ io.err(`Removed webhook ${id}\n`);
106
+ return 0;
107
+ }
108
+ case "test": {
109
+ const id = requireId(args, "Usage: envpass webhooks test <id>");
110
+ const result = await client.testWebhook(id, scope);
111
+ io.out(describeTest(result));
112
+ return result.ok ? 0 : 1;
113
+ }
114
+ case "deliveries": {
115
+ const id = requireId(args, "Usage: envpass webhooks deliveries <id> [--limit N]");
116
+ const deliveries = await client.listWebhookDeliveries(id, { limit: parseLimit(options.limit) }, scope);
117
+ if (options.json) {
118
+ io.out(json(deliveries));
119
+ return 0;
120
+ }
121
+ if (!deliveries.length) {
122
+ io.err("No deliveries yet.\n");
123
+ return 0;
124
+ }
125
+ io.out(table(["ID", "EVENT", "STATUS", "CODE", "ATTEMPTS", "DURATION", "CREATED", "ERROR"], deliveries.map((delivery) => [
126
+ delivery.id,
127
+ delivery.eventType,
128
+ delivery.status,
129
+ delivery.responseStatus === null ? "-" : String(delivery.responseStatus),
130
+ String(delivery.attempts),
131
+ delivery.durationMs === null ? "-" : `${delivery.durationMs}ms`,
132
+ delivery.createdAt,
133
+ oneLine(delivery.error),
134
+ ])));
135
+ return 0;
136
+ }
137
+ default:
138
+ throw new Error(WEBHOOKS_USAGE);
139
+ }
140
+ }
141
+ export async function syncsCommand(client, options, io) {
142
+ const [action, ...args] = options.args;
143
+ const scope = { workspace: options.scope.workspace, project: options.scope.project };
144
+ switch (action) {
145
+ case "list": {
146
+ const syncs = await client.listSyncs(scope);
147
+ if (options.json) {
148
+ io.out(json(syncs));
149
+ return 0;
150
+ }
151
+ if (!syncs.length) {
152
+ io.err("No syncs.\n");
153
+ return 0;
154
+ }
155
+ io.out(table(["ID", "DESTINATION", "ENVIRONMENT", "STATUS", "LAST SYNCED", "LAST ERROR"], syncs.map((sync) => [
156
+ sync.id,
157
+ sync.destination,
158
+ sync.environmentSlug,
159
+ sync.enabled ? sync.status : "paused",
160
+ sync.lastSyncedAt ?? "never",
161
+ oneLine(sync.lastError),
162
+ ])));
163
+ return 0;
164
+ }
165
+ case "run": {
166
+ const id = requireId(args, "Usage: envpass syncs run <id>");
167
+ await client.runSync(id, scope);
168
+ io.err(`Queued sync ${id}\n`);
169
+ return 0;
170
+ }
171
+ case "pause":
172
+ case "resume": {
173
+ const id = requireId(args, `Usage: envpass syncs ${action} <id>`);
174
+ await client.setSyncEnabled(id, action === "resume", scope);
175
+ io.err(`${action === "resume" ? "Resumed" : "Paused"} sync ${id}\n`);
176
+ return 0;
177
+ }
178
+ default:
179
+ throw new Error(SYNCS_USAGE);
180
+ }
181
+ }
package/dist/mask.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Transform } from "node:stream";
2
+ export declare function maskText(text: string, secrets: readonly string[]): string;
3
+ export declare function maskingStream(values: readonly string[]): Transform;
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
+ }