@vaultic-dev/cli 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +63 -0
  2. package/dist/index.js +159 -10
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @vaultic-dev/cli
2
+
3
+ Command-line client for [Vaultic](https://github.com/vaultic-dev/vaultic) — encrypted,
4
+ versioned secrets management with environment inheritance, rotation, and audit logging. This
5
+ package installs the `vaultic` binary; it talks to a Vaultic server over the REST API and never
6
+ handles crypto itself beyond decrypting values already fetched over the wire.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g @vaultic-dev/cli
12
+ vaultic --help
13
+ ```
14
+
15
+ Requires Node.js >= 20. You'll also need a Vaultic server to talk to — either your own
16
+ self-hosted instance, or one your team runs (see the
17
+ [main repo README](https://github.com/vaultic-dev/vaultic#readme) for self-hosting instructions).
18
+
19
+ ## Quick start
20
+
21
+ ```bash
22
+ vaultic login # opens a browser to approve the login (--no-browser for the
23
+ # email/password prompt, --token to paste one)
24
+ vaultic init # interactive: pick/create workspace -> project -> default env
25
+ # writes .vaultic.yaml in the current directory
26
+
27
+ vaultic secrets set FOO bar # stored encrypted server-side, versioned
28
+ vaultic secrets list # masked by default
29
+ vaultic secrets list --reveal # explicit reveal, audited separately from list/read
30
+
31
+ vaultic run -- printenv FOO # injects secrets as env vars into the subprocess -> "bar"
32
+ vaultic export # writes .env (dotenv format) with a checksum header
33
+ vaultic status # compares local .env vs the server
34
+ vaultic sync # pulls latest values, regenerates .env, runs post_sync hook
35
+ ```
36
+
37
+ By default, `vaultic --server <url>` (or the `VAULTIC_API_URL` env var) points at your server;
38
+ otherwise it defaults to `http://localhost:4000`.
39
+
40
+ ## Command reference
41
+
42
+ | Group | Commands |
43
+ |---|---|
44
+ | Auth | `login`, `logout`, `whoami` |
45
+ | Project setup | `init` (writes `.vaultic.yaml`) |
46
+ | Secrets | `secrets list/get/set/delete/history/rollback/rename/rotate`, `secrets override set/clear`, `rotation-providers` |
47
+ | Local file sync | `run`, `export`, `import`, `status`, `sync` |
48
+ | Environments | `env list/create/duplicate/diff/promote/lock/unlock/proposals/approve/reject` |
49
+ | Workspace | `workspace invite/invites/revoke-invite/members/set-role/remove-member/accept-invite` |
50
+ | Access grants | `access grant/list/revoke` |
51
+ | Service tokens | `tokens create/list/revoke` |
52
+ | Sharing | `share <key>` — one-time/limited-view unauthenticated link |
53
+ | Webhooks | `webhooks create/list/delete/deliveries` |
54
+ | Third-party push | `push github`, `push vercel` |
55
+ | Git integration | `git-integration setup/show/remove` — ephemeral preview environments |
56
+
57
+ Run `vaultic <command> --help` for a command's full options.
58
+
59
+ ## Links
60
+
61
+ - [Full documentation, deployment guide, and security model](https://github.com/vaultic-dev/vaultic#readme)
62
+ - [Changelog](https://github.com/vaultic-dev/vaultic/blob/main/CHANGELOG.md)
63
+ - [Issues](https://github.com/vaultic-dev/vaultic/issues)
package/dist/index.js CHANGED
@@ -100,6 +100,11 @@ import { z } from "zod";
100
100
  var SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/;
101
101
  var SlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric with dashes");
102
102
  var SecretKeySchema = z.string().min(1).max(256).regex(SECRET_KEY_REGEX, "keys must be UPPER_SNAKE_CASE (e.g. DATABASE_URL)");
103
+ function normalizeTag(tag) {
104
+ return tag.trim().toLowerCase();
105
+ }
106
+ var TagSchema = z.string().trim().min(1, "tag cannot be empty").max(64, "tag must be 64 characters or fewer").transform(normalizeTag);
107
+ var SecretTagsSchema = z.array(TagSchema).max(50, "a secret can have at most 50 tags").transform((tags) => Array.from(new Set(tags)));
103
108
  var RoleSchema = z.enum(["owner", "admin", "developer", "read-only"]);
104
109
  var TokenAccessSchema = z.enum(["read", "write"]);
105
110
  var SecretTypeHintSchema = z.enum([
@@ -227,7 +232,11 @@ var SecretSummarySchema = z.object({
227
232
  /** when this secret's value was last changed — `rotatedAt` if it's ever gone through
228
233
  * `rotate`, otherwise the most recent version's createdAt. Used to compute rotation-due
229
234
  * state (see `isRotationDue`) client-side, in the CLI, and by the scheduled lifecycle job. */
230
- lastRotatedAt: z.string().nullable().optional()
235
+ lastRotatedAt: z.string().nullable().optional(),
236
+ /** freeform, normalized (lowercase + trim) tags assigned to this secret (spec: "Tags on
237
+ * secrets + tag-based search"). Visible to anyone who can see the secret at all — not
238
+ * sensitive, same as key names. */
239
+ tags: z.array(z.string()).optional()
231
240
  });
232
241
  var SecretRevealedSchema = SecretSummarySchema.extend({
233
242
  value: z.string()
@@ -315,6 +324,38 @@ var AuditLogEntrySchema = z.object({
315
324
  ip: z.string().nullable(),
316
325
  createdAt: z.string()
317
326
  });
327
+ var NotificationSchema = z.object({
328
+ id: z.string(),
329
+ type: z.string(),
330
+ refId: z.string().nullable(),
331
+ refType: z.string().nullable(),
332
+ data: z.record(z.string(), z.unknown()).nullable(),
333
+ read: z.boolean(),
334
+ readAt: z.string().nullable(),
335
+ createdAt: z.string()
336
+ });
337
+ var NotificationListResponseSchema = z.object({
338
+ notifications: z.array(NotificationSchema),
339
+ unreadCount: z.number()
340
+ });
341
+ var SecretCommentSchema = z.object({
342
+ id: z.string(),
343
+ secretId: z.string(),
344
+ authorId: z.string().nullable(),
345
+ authorName: z.string().nullable(),
346
+ authorEmail: z.string().nullable(),
347
+ body: z.string(),
348
+ createdAt: z.string(),
349
+ /** null until the comment has been edited at least once */
350
+ editedAt: z.string().nullable()
351
+ });
352
+ var SecretCommentBodySchema = z.string().trim().min(1, "comment cannot be empty").max(4e3, "comment must be 4000 characters or fewer");
353
+ var CreateSecretCommentRequestSchema = z.object({
354
+ body: SecretCommentBodySchema
355
+ });
356
+ var UpdateSecretCommentRequestSchema = z.object({
357
+ body: SecretCommentBodySchema
358
+ });
318
359
  var RegisterRequestSchema = z.object({
319
360
  email: z.string().email(),
320
361
  password: z.string().min(8),
@@ -377,11 +418,17 @@ var SetSecretRequestSchema = z.object({
377
418
  expiresAt: z.string().datetime().nullable().optional(),
378
419
  /** omit to leave unchanged, null to clear, a positive integer (days) to set (see
379
420
  * "Rotation reminders" in the spec) */
380
- rotationReminderIntervalDays: z.number().int().positive().nullable().optional()
421
+ rotationReminderIntervalDays: z.number().int().positive().nullable().optional(),
422
+ /** omit to leave tags unchanged, an array (possibly empty, to clear all tags) to replace
423
+ * the full tag set — normalized/deduped by SecretTagsSchema (see "Tags on secrets"). */
424
+ tags: SecretTagsSchema.optional()
381
425
  });
382
426
  var UpdateSecretMetadataRequestSchema = z.object({
383
427
  expiresAt: z.string().datetime().nullable().optional(),
384
- rotationReminderIntervalDays: z.number().int().positive().nullable().optional()
428
+ rotationReminderIntervalDays: z.number().int().positive().nullable().optional(),
429
+ /** omit to leave tags unchanged, an array (possibly empty, to clear all tags) to replace
430
+ * the full tag set. */
431
+ tags: SecretTagsSchema.optional()
385
432
  });
386
433
  var RenameSecretRequestSchema = z.object({
387
434
  newKey: SecretKeySchema
@@ -558,6 +605,20 @@ var SecretsHealthSchema = z.object({
558
605
  expiredReminders: z.array(ExpiredReminderSchema),
559
606
  inactiveConfigs: z.array(InactiveConfigSchema)
560
607
  });
608
+ var DuplicateValueMemberSchema = SecretsHealthLocationSchema.extend({
609
+ key: z.string()
610
+ });
611
+ var DuplicateValueGroupSchema = z.object({
612
+ count: z.number().int(),
613
+ members: z.array(DuplicateValueMemberSchema)
614
+ });
615
+ var DuplicateValuesResponseSchema = z.object({
616
+ groups: z.array(DuplicateValueGroupSchema),
617
+ /** true if at least one visible secret's current version hasn't been HMAC-indexed yet
618
+ * (null valueHmac) — e.g. a version written before this feature existed. Lets the UI show
619
+ * a "results may be incomplete" notice instead of silently under-reporting duplicates. */
620
+ incomplete: z.boolean()
621
+ });
561
622
 
562
623
  // src/lib/project-config.ts
563
624
  var CONFIG_FILE = ".vaultic.yaml";
@@ -724,6 +785,88 @@ function exitCancelled() {
724
785
 
725
786
  // src/commands/auth.ts
726
787
  import prompts2 from "prompts";
788
+
789
+ // src/lib/browser-login.ts
790
+ import { createServer } from "http";
791
+ import { randomBytes } from "crypto";
792
+ import { spawn } from "child_process";
793
+ function openInBrowser(url) {
794
+ try {
795
+ const platform = process.platform;
796
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
797
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
798
+ } catch {
799
+ }
800
+ }
801
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
802
+ async function browserLogin(opts) {
803
+ const { serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS, onUrl } = opts;
804
+ const providers = await fetch(`${serverUrl}/auth/providers`).then((res) => res.ok ? res.json() : null).catch(() => null);
805
+ const webAppUrl = providers?.webAppUrl;
806
+ if (!webAppUrl) {
807
+ throw new Error(
808
+ "This server doesn't support browser login (no webAppUrl from /auth/providers) -- use --email/--password or --token instead."
809
+ );
810
+ }
811
+ const state = randomBytes(24).toString("hex");
812
+ return new Promise((resolve, reject) => {
813
+ let settled = false;
814
+ const timeout = setTimeout(() => {
815
+ if (settled) return;
816
+ settled = true;
817
+ server.close();
818
+ reject(new Error("Timed out waiting for browser login approval."));
819
+ }, timeoutMs);
820
+ const server = createServer((req, res) => {
821
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
822
+ if (url.pathname !== "/callback") {
823
+ res.writeHead(404).end();
824
+ return;
825
+ }
826
+ const returnedState = url.searchParams.get("state");
827
+ const code = url.searchParams.get("code");
828
+ const finish = (err, result) => {
829
+ if (settled) return;
830
+ settled = true;
831
+ clearTimeout(timeout);
832
+ server.close();
833
+ if (err) reject(err);
834
+ else resolve(result);
835
+ };
836
+ if (returnedState !== state || !code) {
837
+ res.writeHead(400, { "content-type": "text/html" }).end("<h1>Invalid login attempt.</h1>You can close this tab.");
838
+ finish(new Error("Received an invalid or mismatched login callback."));
839
+ return;
840
+ }
841
+ fetch(`${serverUrl}/auth/cli/exchange`, {
842
+ method: "POST",
843
+ headers: { "content-type": "application/json" },
844
+ body: JSON.stringify({ code })
845
+ }).then(async (exchangeRes) => {
846
+ const data = await exchangeRes.json();
847
+ if (!exchangeRes.ok) throw new Error(data?.error ?? "Login failed");
848
+ res.writeHead(200, { "content-type": "text/html" }).end("<h1>Logged in!</h1>You can close this tab and return to your terminal.");
849
+ finish(null, data);
850
+ }).catch((err) => {
851
+ res.writeHead(500, { "content-type": "text/html" }).end("<h1>Login failed.</h1>You can close this tab.");
852
+ finish(err instanceof Error ? err : new Error(String(err)));
853
+ });
854
+ });
855
+ server.listen(0, "127.0.0.1", () => {
856
+ const port = server.address().port;
857
+ const loginUrl = `${webAppUrl}/cli-login?state=${state}&port=${port}`;
858
+ if (onUrl) {
859
+ onUrl(loginUrl);
860
+ } else {
861
+ console.log(`Opening ${loginUrl} in your browser\u2026`);
862
+ console.log("Waiting for you to approve the login (Ctrl+C to cancel)\u2026");
863
+ openInBrowser(loginUrl);
864
+ }
865
+ });
866
+ });
867
+ }
868
+
869
+ // src/commands/auth.ts
727
870
  async function loginCommand(opts) {
728
871
  const serverUrl = opts.server ?? process.env.VAULTIC_API_URL ?? DEFAULT_SERVER_URL;
729
872
  if (opts.token) {
@@ -731,6 +874,12 @@ async function loginCommand(opts) {
731
874
  console.log(`Saved credentials for ${serverUrl}`);
732
875
  return;
733
876
  }
877
+ if (!opts.email && !opts.password && opts.browser !== false) {
878
+ const res = await browserLogin({ serverUrl });
879
+ saveCredentials({ serverUrl, token: res.token, email: res.user.email });
880
+ console.log(`Logged in as ${res.user.email}`);
881
+ return;
882
+ }
734
883
  const email = opts.email ?? (await prompts2({ type: "text", name: "email", message: "Email" })).email;
735
884
  const password = opts.password ?? (await prompts2({ type: "password", name: "password", message: "Password" })).password;
736
885
  if (!email || !password) {
@@ -766,7 +915,7 @@ async function whoamiCommand() {
766
915
  console.log("Not logged in.");
767
916
  return;
768
917
  }
769
- if (creds.token.startsWith("vlt_")) {
918
+ if (creds.token.startsWith("sht_")) {
770
919
  console.log(`Authenticated with a service token against ${creds.serverUrl}`);
771
920
  return;
772
921
  }
@@ -776,7 +925,7 @@ async function whoamiCommand() {
776
925
  }
777
926
 
778
927
  // src/commands/run.ts
779
- import { spawn } from "child_process";
928
+ import { spawn as spawn2 } from "child_process";
780
929
 
781
930
  // src/lib/context.ts
782
931
  function envBase(cfg, env2) {
@@ -802,7 +951,7 @@ async function runCommand(commandParts, opts) {
802
951
  const fetchSecrets = () => api.get(`${envBase(cfg, env2)}/export`);
803
952
  if (!opts.watch) {
804
953
  const secrets2 = await fetchSecrets();
805
- const child2 = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...secrets2 } });
954
+ const child2 = spawn2(cmd, args, { stdio: "inherit", env: { ...process.env, ...secrets2 } });
806
955
  child2.on("exit", (code, signal) => {
807
956
  if (signal) process.kill(process.pid, signal);
808
957
  else process.exit(code ?? 0);
@@ -819,7 +968,7 @@ async function runCommand(commandParts, opts) {
819
968
  let stopped = false;
820
969
  let restarting = false;
821
970
  function spawnChild() {
822
- child = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...current } });
971
+ child = spawn2(cmd, args, { stdio: "inherit", env: { ...process.env, ...current } });
823
972
  child.on("exit", (code, signal) => {
824
973
  if (stopped || restarting) return;
825
974
  stopped = true;
@@ -1094,11 +1243,11 @@ async function syncCommand(opts) {
1094
1243
  import readline from "readline";
1095
1244
 
1096
1245
  // src/rotation/postgres.ts
1097
- import { randomBytes } from "crypto";
1246
+ import { randomBytes as randomBytes2 } from "crypto";
1098
1247
  import { Client } from "pg";
1099
1248
  var ROLE_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
1100
1249
  function generatePassword(bytes = 24) {
1101
- return randomBytes(bytes).toString("base64url");
1250
+ return randomBytes2(bytes).toString("base64url");
1102
1251
  }
1103
1252
  function sqlLiteral(value) {
1104
1253
  return `'${value.replace(/'/g, "''")}'`;
@@ -1956,7 +2105,7 @@ async function gitIntegrationRemoveCommand() {
1956
2105
  var program = new Command();
1957
2106
  program.name("vaultic").description("Vaultic CLI").version("0.1.0");
1958
2107
  program.command("init").description("Initialize this directory as a Vaultic project (writes .vaultic.yaml)").action(wrap(initCommand));
1959
- program.command("login").description("Log in with email/password, or paste a token with --token").option("--token <token>", "paste a session or service token instead of logging in interactively").option("--server <url>", "Vaultic server URL").option("--email <email>", "email (non-interactive)").option("--password <password>", "password (non-interactive)").action(wrap(loginCommand));
2108
+ program.command("login").description("Log in via your browser (default), with email/password, or by pasting a token with --token").option("--token <token>", "paste a session or service token instead of logging in interactively").option("--server <url>", "Vaultic server URL").option("--email <email>", "email (non-interactive; skips the browser flow)").option("--password <password>", "password (non-interactive; skips the browser flow)").option("--no-browser", "skip opening a browser; prompt for email/password instead").action(wrap(loginCommand));
1960
2109
  program.command("logout").description("Clear stored credentials").action(wrap(logoutCommand));
1961
2110
  program.command("whoami").description("Show the current logged-in user").action(wrap(whoamiCommand));
1962
2111
  program.command("run").description("Inject secrets as env vars and run a command").option("-e, --env <environment>", "environment to use").option("--watch", "poll for secret changes and restart the subprocess when they change").option("--poll-interval <ms>", "watch poll interval in milliseconds (default 2000)").allowUnknownOption().argument("<command...>", "command to run, after --").action(wrap((commandParts, opts) => runCommand(commandParts, opts)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaultic-dev/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vaultic": "./dist/index.js"