envpass-cli 0.1.6 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # envpass-cli
2
2
 
3
- Client library and CLI for [envpass](https://envpass.dev): encrypted environment variables for every project, service and environment.
3
+ Client library and CLI for [envpass](https://envpass.dev): encrypted environment variables for every project and environment.
4
4
 
5
5
  ## CLI
6
6
 
@@ -10,6 +10,8 @@ bunx envpass-cli link -w acme -p shop -e dev # save defaults to .envpass.json
10
10
  bunx envpass-cli pull # write .env
11
11
  bunx envpass-cli pull -e prod -o - # print to stdout
12
12
  bunx envpass-cli run -e dev -- bun dev # run with values injected, masked in output
13
+ bunx envpass-cli webhooks list # webhooks and syncs need `login`
14
+ bunx envpass-cli syncs run <id> # push a sync now
13
15
  ```
14
16
 
15
17
  `npx envpass-cli …` works the same way.
@@ -25,7 +27,6 @@ ENVPASS_TOKEN=ep_live_… npx envpass-cli pull -e prod
25
27
  | `-w, --workspace` | `ENVPASS_WORKSPACE` | Workspace slug (not needed with a service token) |
26
28
  | `-p, --project` | `ENVPASS_PROJECT` | Project slug (not needed with a service token) |
27
29
  | `-e, --env` | `ENVPASS_ENV` | Environment slug |
28
- | `-s, --service` | `ENVPASS_SERVICE` | Service slug, or `shared` |
29
30
  | `--url` | `ENVPASS_URL` | Server, default `https://envpass.dev` |
30
31
  | | `ENVPASS_TOKEN` | Service token or CLI session token |
31
32
 
@@ -37,7 +38,8 @@ import { createEnvpass, loadEnv } from "envpass-cli"
37
38
  await loadEnv({ environment: "prod" })
38
39
 
39
40
  const envpass = createEnvpass({ token: process.env.ENVPASS_TOKEN })
40
- const entries = await envpass.resolve({ environment: "prod", service: "api" })
41
+ const entries = await envpass.resolve({ environment: "prod" })
42
+ const webhooks = await envpass.listWebhooks({ workspace: "acme" })
41
43
 
42
44
  const { data } = await envpass.api.GET("/api/v1/workspaces")
43
45
  ```
package/dist/cli.js CHANGED
@@ -6,9 +6,10 @@ import { parseArgs } from "node:util";
6
6
  import { createEnvpass, EnvpassError } from "./client.js";
7
7
  import { baseUrl, forgetToken, resolveScope, saveToken, storedToken, writeProjectLink, } from "./config.js";
8
8
  import { deviceLogin, openBrowser } from "./device.js";
9
+ import { syncsCommand, webhooksCommand } from "./integrations.js";
9
10
  import { maskingStream } from "./mask.js";
10
11
  const { version } = createRequire(import.meta.url)("../package.json");
11
- const HELP = `envpass ${version}: encrypted environment variables for every project, service and environment
12
+ const HELP = `envpass ${version}: encrypted environment variables for every project and environment
12
13
 
13
14
  Usage
14
15
  envpass login Sign in through your browser
@@ -18,20 +19,41 @@ Usage
18
19
  envpass pull [-e env] [-o .env] Write resolved values to a file ("-" for stdout)
19
20
  envpass run [-e env] -- <command> Run a command with the values injected
20
21
 
22
+ Webhooks and syncs (need \`envpass login\`, not a service token)
23
+ envpass webhooks list List the workspace's webhooks
24
+ envpass webhooks add --url <url> Register a webhook and print its signing secret once
25
+ envpass webhooks remove <id> Delete a webhook
26
+ envpass webhooks test <id> Send a test event (exit code 1 if it fails)
27
+ envpass webhooks deliveries <id> Show a webhook's recent deliveries
28
+ envpass syncs list [-p project] List the project's syncs
29
+ envpass syncs run <id> Push a sync now
30
+ envpass syncs pause <id> Stop a sync from running
31
+ envpass syncs resume <id> Turn a paused sync back on
32
+
21
33
  Options
22
34
  -w, --workspace <slug> Workspace (not needed with a service token)
23
35
  -p, --project <slug> Project (not needed with a service token)
24
36
  -e, --env <slug> Environment, e.g. dev or prod
25
- -s, --service <slug> Service layer, or "shared"
26
37
  -o, --out <file> pull: output file (default .env)
27
38
  -f, --format <format> pull: dotenv, shell or json (default dotenv)
28
39
  --no-mask run: show secret values in the command's output
29
40
  --no-browser login: print the link instead of opening a browser
41
+ --json webhooks list, webhooks deliveries, syncs list: print raw JSON
42
+ --limit <n> webhooks deliveries: how many to show, 1-100 (default 25)
30
43
  --url <url> envpass server (default https://envpass.dev, or ENVPASS_URL)
31
44
 
45
+ Options for webhooks add
46
+ --url <url> Endpoint to POST to (point at your server with ENVPASS_URL)
47
+ --event <event> secrets.changed, sync.succeeded or sync.failed; repeat for more
48
+ (default secrets.changed and sync.failed)
49
+ -e, --env <slug> Only these environments; repeat for more (default all)
50
+ -p, --project <slug> Only these projects; repeat for more (default all)
51
+ -f, --format <format> json or slack (default slack for Slack URLs, json otherwise)
52
+ --description <text> A note to tell webhooks apart
53
+
32
54
  Environment
33
55
  ENVPASS_TOKEN A service token (ep_live_…) or CLI session token
34
- ENVPASS_WORKSPACE, ENVPASS_PROJECT, ENVPASS_ENV, ENVPASS_SERVICE
56
+ ENVPASS_WORKSPACE, ENVPASS_PROJECT, ENVPASS_ENV
35
57
  `;
36
58
  function fail(message, code = 1) {
37
59
  process.stderr.write(`envpass: ${message}\n`);
@@ -43,12 +65,15 @@ async function main(argv) {
43
65
  allowPositionals: true,
44
66
  options: {
45
67
  workspace: { type: "string", short: "w" },
46
- project: { type: "string", short: "p" },
47
- env: { type: "string", short: "e" },
48
- service: { type: "string", short: "s" },
68
+ project: { type: "string", short: "p", multiple: true },
69
+ env: { type: "string", short: "e", multiple: true },
49
70
  out: { type: "string", short: "o" },
50
71
  format: { type: "string", short: "f" },
51
72
  url: { type: "string" },
73
+ event: { type: "string", multiple: true },
74
+ description: { type: "string" },
75
+ limit: { type: "string" },
76
+ json: { type: "boolean" },
52
77
  "no-mask": { type: "boolean" },
53
78
  "no-browser": { type: "boolean" },
54
79
  help: { type: "boolean", short: "h" },
@@ -60,12 +85,12 @@ async function main(argv) {
60
85
  return void process.stdout.write(`${version}\n`);
61
86
  if (values.help || !command || command === "help")
62
87
  return void process.stdout.write(HELP);
63
- const url = baseUrl(values.url);
88
+ const endpointUrl = command === "webhooks" && rest[0] === "add";
89
+ const url = baseUrl(endpointUrl ? undefined : values.url);
64
90
  const flags = {
65
91
  workspace: values.workspace,
66
- project: values.project,
67
- environment: values.env,
68
- service: values.service,
92
+ project: values.project?.at(-1),
93
+ environment: values.env?.at(-1),
69
94
  };
70
95
  switch (command) {
71
96
  case "login": {
@@ -172,6 +197,29 @@ async function main(argv) {
172
197
  });
173
198
  return;
174
199
  }
200
+ case "webhooks":
201
+ case "syncs": {
202
+ const client = createEnvpass({ baseUrl: url });
203
+ if (!client.hasToken)
204
+ fail("Not signed in. Run `envpass login` or set ENVPASS_TOKEN.");
205
+ const run = command === "webhooks" ? webhooksCommand : syncsCommand;
206
+ process.exitCode = await run(client, {
207
+ scope: flags,
208
+ args: rest,
209
+ json: values.json,
210
+ url: endpointUrl ? values.url : undefined,
211
+ events: values.event,
212
+ environments: values.env,
213
+ projects: values.project,
214
+ format: values.format,
215
+ description: values.description,
216
+ limit: values.limit,
217
+ }, {
218
+ out: (text) => process.stdout.write(text),
219
+ err: (text) => process.stderr.write(text),
220
+ });
221
+ return;
222
+ }
175
223
  default:
176
224
  fail(`Unknown command "${command}". Run \`envpass --help\`.`);
177
225
  }
package/dist/client.d.ts CHANGED
@@ -1,16 +1,36 @@
1
1
  import { type ProjectLink } from "./config.js";
2
2
  import type { components, paths } from "./schema.js";
3
- export type Source = components["schemas"]["SecretSource"];
3
+ type Schemas = components["schemas"];
4
+ export type Source = Schemas["SecretSource"];
4
5
  export type ResolvedEntry = {
5
6
  key: string;
6
7
  value: string;
7
8
  source: Source;
8
9
  };
9
10
  export type ExportFormat = "dotenv" | "json" | "shell";
11
+ export type Workspace = Schemas["Workspace"];
12
+ export type Environment = Schemas["Environment"];
13
+ export type Webhook = Schemas["Webhook"];
14
+ export type WebhookEvent = Schemas["WebhookEvent"];
15
+ export type WebhookFormat = Webhook["format"];
16
+ export type WebhookDelivery = Schemas["WebhookDelivery"];
17
+ export type WebhookTestResult = Schemas["WebhookTestResult"];
18
+ export type Connection = Schemas["Connection"];
19
+ export type Sync = Schemas["Sync"];
20
+ export type NewWebhook = {
21
+ url: string;
22
+ events?: WebhookEvent[];
23
+ environments?: string[];
24
+ projects?: string[];
25
+ format?: WebhookFormat;
26
+ description?: string | null;
27
+ };
28
+ export declare const WEBHOOK_EVENTS: readonly WebhookEvent[];
10
29
  export type EnvpassOptions = {
11
30
  baseUrl?: string;
12
31
  token?: string;
13
32
  userAgent?: string;
33
+ fetch?: typeof fetch;
14
34
  };
15
35
  export declare class EnvpassError extends Error {
16
36
  readonly status: number;
@@ -28,7 +48,6 @@ export declare function createEnvpass(options?: EnvpassOptions): {
28
48
  id: string;
29
49
  workspaceId: string;
30
50
  projectId: string;
31
- serviceId: string | null;
32
51
  access: "read" | "write";
33
52
  } | null;
34
53
  workspaces: {
@@ -40,6 +59,23 @@ export declare function createEnvpass(options?: EnvpassOptions): {
40
59
  }>;
41
60
  resolve(scope?: ProjectLink): Promise<ResolvedEntry[]>;
42
61
  resolveText(scope?: ProjectLink, format?: Exclude<ExportFormat, "json">): Promise<string>;
62
+ workspace(scope?: ProjectLink): Promise<Workspace>;
63
+ listEnvironments(scope?: ProjectLink): Promise<Environment[]>;
64
+ listWebhooks(scope?: ProjectLink): Promise<Webhook[]>;
65
+ createWebhook(input: NewWebhook, scope?: ProjectLink): Promise<{
66
+ webhook: Webhook;
67
+ secret: string;
68
+ }>;
69
+ deleteWebhook(id: string, scope?: ProjectLink): Promise<void>;
70
+ testWebhook(id: string, scope?: ProjectLink): Promise<WebhookTestResult>;
71
+ listWebhookDeliveries(id: string, options?: {
72
+ limit?: number;
73
+ before?: string;
74
+ }, scope?: ProjectLink): Promise<WebhookDelivery[]>;
75
+ listConnections(scope?: ProjectLink): Promise<Connection[]>;
76
+ listSyncs(scope?: ProjectLink): Promise<Sync[]>;
77
+ runSync(id: string, scope?: ProjectLink): Promise<void>;
78
+ setSyncEnabled(id: string, enabled: boolean, scope?: ProjectLink): Promise<void>;
43
79
  };
44
80
  export type Envpass = ReturnType<typeof createEnvpass>;
45
81
  export declare function loadEnv(scope?: ProjectLink & EnvpassOptions & {
@@ -47,3 +83,4 @@ export declare function loadEnv(scope?: ProjectLink & EnvpassOptions & {
47
83
  }): Promise<{
48
84
  [k: string]: string;
49
85
  }>;
86
+ export {};
package/dist/client.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import createFetchClient from "openapi-fetch";
2
2
  import { baseUrl, resolveScope, resolveToken } from "./config.js";
3
+ export const WEBHOOK_EVENTS = [
4
+ "secrets.changed",
5
+ "sync.succeeded",
6
+ "sync.failed",
7
+ ];
8
+ const DEFAULT_WEBHOOK_EVENTS = ["secrets.changed", "sync.failed"];
3
9
  export class EnvpassError extends Error {
4
10
  status;
5
11
  code;
@@ -14,13 +20,27 @@ function errorFrom(status, body) {
14
20
  const error = body?.error;
15
21
  return new EnvpassError(error?.message ?? `Request failed with status ${status}`, status, error?.code);
16
22
  }
23
+ function unwrap(result) {
24
+ if (result.data === undefined)
25
+ throw errorFrom(result.response.status, result.error);
26
+ return result.data;
27
+ }
28
+ function pickIds(items, refs, kind, workspace) {
29
+ return refs.map((ref) => {
30
+ const item = items.find((candidate) => candidate.slug === ref || candidate.id === ref);
31
+ if (!item)
32
+ throw new EnvpassError(`No ${kind} "${ref}" in ${workspace.slug}`, 404, "not_found");
33
+ return item.id;
34
+ });
35
+ }
17
36
  export function createEnvpass(options = {}) {
18
37
  const url = baseUrl(options.baseUrl);
19
38
  const token = resolveToken(url, options.token);
20
39
  const headers = { "user-agent": options.userAgent ?? "envpass-node" };
21
40
  if (token)
22
41
  headers.authorization = `Bearer ${token}`;
23
- const api = createFetchClient({ baseUrl: url, headers });
42
+ const fetcher = options.fetch ?? globalThis.fetch;
43
+ const api = createFetchClient({ baseUrl: url, headers, fetch: fetcher });
24
44
  function scopeQuery(scope) {
25
45
  if (!scope.environment) {
26
46
  throw new EnvpassError("No environment. Pass --env, set ENVPASS_ENV, or run `envpass link`.", 400, "invalid");
@@ -29,9 +49,52 @@ export function createEnvpass(options = {}) {
29
49
  workspace: scope.workspace,
30
50
  project: scope.project,
31
51
  environment: scope.environment,
32
- service: scope.service,
33
52
  };
34
53
  }
54
+ function requireUser() {
55
+ if (token?.startsWith("ep_")) {
56
+ throw new EnvpassError("Webhooks and syncs need a user login, not a service token. Run `envpass login` and unset ENVPASS_TOKEN.", 403, "forbidden");
57
+ }
58
+ }
59
+ async function workspaceFor(scope) {
60
+ requireUser();
61
+ const ref = resolveScope(scope).workspace;
62
+ const { workspaces } = unwrap(await api.GET("/api/v1/workspaces"));
63
+ if (ref) {
64
+ const match = workspaces.find((w) => w.slug === ref || w.id === ref);
65
+ if (!match)
66
+ throw new EnvpassError(`No workspace "${ref}" for this login`, 404, "not_found");
67
+ return match;
68
+ }
69
+ const [only] = workspaces;
70
+ if (only && workspaces.length === 1)
71
+ return only;
72
+ throw new EnvpassError("No workspace. Pass --workspace, set ENVPASS_WORKSPACE, or run `envpass link`.", 400, "invalid");
73
+ }
74
+ async function projectsOf(workspaceId) {
75
+ const path = { workspaceId };
76
+ return unwrap(await api.GET("/api/v1/workspaces/{workspaceId}/projects", { params: { path } }))
77
+ .projects;
78
+ }
79
+ async function environmentsOf(workspaceId) {
80
+ const path = { workspaceId };
81
+ return unwrap(await api.GET("/api/v1/workspaces/{workspaceId}/environments", { params: { path } })).environments;
82
+ }
83
+ async function projectFor(scope) {
84
+ const workspace = await workspaceFor(scope);
85
+ const ref = resolveScope(scope).project;
86
+ if (!ref) {
87
+ throw new EnvpassError("No project. Pass --project, set ENVPASS_PROJECT, or run `envpass link`.", 400, "invalid");
88
+ }
89
+ const [projectId = ref] = pickIds(await projectsOf(workspace.id), [ref], "project", workspace);
90
+ return { workspaceId: workspace.id, projectId };
91
+ }
92
+ async function webhookPath(webhookId, scope) {
93
+ return { workspaceId: (await workspaceFor(scope)).id, webhookId };
94
+ }
95
+ async function syncPath(syncId, scope) {
96
+ return { ...(await projectFor(scope)), syncId };
97
+ }
35
98
  return {
36
99
  url,
37
100
  hasToken: Boolean(token),
@@ -53,7 +116,7 @@ export function createEnvpass(options = {}) {
53
116
  },
54
117
  async resolveText(scope = {}, format = "dotenv") {
55
118
  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 });
119
+ const response = await fetcher(`${url}/api/v1/resolve?${new URLSearchParams(Object.entries({ ...query, format }).filter((entry) => Boolean(entry[1])))}`, { headers });
57
120
  const text = await response.text();
58
121
  if (!response.ok) {
59
122
  let body;
@@ -67,6 +130,81 @@ export function createEnvpass(options = {}) {
67
130
  }
68
131
  return text;
69
132
  },
133
+ async workspace(scope = {}) {
134
+ return workspaceFor(scope);
135
+ },
136
+ async listEnvironments(scope = {}) {
137
+ return environmentsOf((await workspaceFor(scope)).id);
138
+ },
139
+ async listWebhooks(scope = {}) {
140
+ const path = { workspaceId: (await workspaceFor(scope)).id };
141
+ return unwrap(await api.GET("/api/v1/workspaces/{workspaceId}/webhooks", { params: { path } })).webhooks;
142
+ },
143
+ async createWebhook(input, scope = {}) {
144
+ const workspace = await workspaceFor(scope);
145
+ const environments = input.environments ?? [];
146
+ const projects = input.projects ?? [];
147
+ const [environmentIds, projectIds] = await Promise.all([
148
+ environments.length
149
+ ? environmentsOf(workspace.id).then((all) => pickIds(all, environments, "environment", workspace))
150
+ : [],
151
+ projects.length
152
+ ? projectsOf(workspace.id).then((all) => pickIds(all, projects, "project", workspace))
153
+ : [],
154
+ ]);
155
+ return unwrap(await api.POST("/api/v1/workspaces/{workspaceId}/webhooks", {
156
+ params: { path: { workspaceId: workspace.id } },
157
+ body: {
158
+ url: input.url,
159
+ events: input.events?.length ? input.events : DEFAULT_WEBHOOK_EVENTS,
160
+ environmentIds,
161
+ projectIds,
162
+ format: input.format,
163
+ description: input.description,
164
+ },
165
+ }));
166
+ },
167
+ async deleteWebhook(id, scope = {}) {
168
+ const path = await webhookPath(id, scope);
169
+ unwrap(await api.DELETE("/api/v1/workspaces/{workspaceId}/webhooks/{webhookId}", {
170
+ params: { path },
171
+ }));
172
+ },
173
+ async testWebhook(id, scope = {}) {
174
+ const path = await webhookPath(id, scope);
175
+ return unwrap(await api.POST("/api/v1/workspaces/{workspaceId}/webhooks/{webhookId}/test", {
176
+ params: { path },
177
+ }));
178
+ },
179
+ async listWebhookDeliveries(id, options = {}, scope = {}) {
180
+ const path = await webhookPath(id, scope);
181
+ return unwrap(await api.GET("/api/v1/workspaces/{workspaceId}/webhooks/{webhookId}/deliveries", {
182
+ params: { path, query: options },
183
+ })).deliveries;
184
+ },
185
+ async listConnections(scope = {}) {
186
+ const path = { workspaceId: (await workspaceFor(scope)).id };
187
+ return unwrap(await api.GET("/api/v1/workspaces/{workspaceId}/connections", { params: { path } })).connections;
188
+ },
189
+ async listSyncs(scope = {}) {
190
+ const path = await projectFor(scope);
191
+ return unwrap(await api.GET("/api/v1/workspaces/{workspaceId}/projects/{projectId}/syncs", {
192
+ params: { path },
193
+ })).syncs;
194
+ },
195
+ async runSync(id, scope = {}) {
196
+ const path = await syncPath(id, scope);
197
+ unwrap(await api.POST("/api/v1/workspaces/{workspaceId}/projects/{projectId}/syncs/{syncId}/run", {
198
+ params: { path },
199
+ }));
200
+ },
201
+ async setSyncEnabled(id, enabled, scope = {}) {
202
+ const path = await syncPath(id, scope);
203
+ unwrap(await api.PATCH("/api/v1/workspaces/{workspaceId}/projects/{projectId}/syncs/{syncId}", {
204
+ params: { path },
205
+ body: { enabled },
206
+ }));
207
+ },
70
208
  };
71
209
  }
72
210
  export async function loadEnv(scope = {}) {
package/dist/config.d.ts CHANGED
@@ -3,7 +3,6 @@ export type ProjectLink = {
3
3
  workspace?: string;
4
4
  project?: string;
5
5
  environment?: string;
6
- service?: string;
7
6
  };
8
7
  export declare function baseUrl(explicit?: string): string;
9
8
  export declare function saveToken(url: string, token: string): string;
package/dist/config.js CHANGED
@@ -82,6 +82,5 @@ export function resolveScope(flags, start) {
82
82
  workspace: flags.workspace ?? process.env.ENVPASS_WORKSPACE ?? file.workspace,
83
83
  project: flags.project ?? process.env.ENVPASS_PROJECT ?? file.project,
84
84
  environment: flags.environment ?? process.env.ENVPASS_ENV ?? file.environment,
85
- service: flags.service ?? process.env.ENVPASS_SERVICE ?? file.service,
86
85
  };
87
86
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createEnvpass, type Envpass, EnvpassError, type EnvpassOptions, type ExportFormat, loadEnv, type ResolvedEntry, type Source, } from "./client.js";
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
2
  export { DEFAULT_URL, type ProjectLink } from "./config.js";
3
3
  export { maskText } from "./mask.js";
4
4
  export type { components, paths } from "./schema.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { createEnvpass, EnvpassError, loadEnv, } from "./client.js";
1
+ export { createEnvpass, EnvpassError, loadEnv, WEBHOOK_EVENTS, } from "./client.js";
2
2
  export { DEFAULT_URL } from "./config.js";
3
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
+ }