@vaultic-dev/cli 0.1.0 → 0.1.2
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 +63 -0
- package/dist/index.js +186 -84
- package/package.json +10 -3
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,14 @@ 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(),
|
|
240
|
+
/** number of entries in this secret's comment thread (see SecretComment) — surfaced on the
|
|
241
|
+
* row so the comment/mention feature is discoverable without opening the panel first. */
|
|
242
|
+
commentCount: z.number().int().optional()
|
|
231
243
|
});
|
|
232
244
|
var SecretRevealedSchema = SecretSummarySchema.extend({
|
|
233
245
|
value: z.string()
|
|
@@ -259,6 +271,20 @@ var RotateSecretRequestSchema = z.object({
|
|
|
259
271
|
* `secrets get KEY --previous`. Omit for no time limit. */
|
|
260
272
|
graceWindow: z.string().optional()
|
|
261
273
|
});
|
|
274
|
+
var ExecuteRotationRequestSchema = z.object({
|
|
275
|
+
/** rotation provider id, e.g. "postgres" — must be one the server implements */
|
|
276
|
+
provider: z.string(),
|
|
277
|
+
/** the secret's current (pre-rotation) resolved value, e.g. a connection string */
|
|
278
|
+
currentValue: z.string(),
|
|
279
|
+
/** provider-specific flags, e.g. { role, adminConnString, newPassword } for postgres */
|
|
280
|
+
options: z.record(z.string().optional()).optional()
|
|
281
|
+
});
|
|
282
|
+
var ExecuteRotationResponseSchema = z.object({
|
|
283
|
+
/** the new value to store as the secret's next version */
|
|
284
|
+
newValue: z.string(),
|
|
285
|
+
/** one-line human-readable description of what was rotated, printed by the CLI */
|
|
286
|
+
summary: z.string()
|
|
287
|
+
});
|
|
262
288
|
function parseDurationMs(spec) {
|
|
263
289
|
const match = /^(\d+)(s|m|h|d)$/.exec(spec.trim());
|
|
264
290
|
if (!match)
|
|
@@ -315,6 +341,38 @@ var AuditLogEntrySchema = z.object({
|
|
|
315
341
|
ip: z.string().nullable(),
|
|
316
342
|
createdAt: z.string()
|
|
317
343
|
});
|
|
344
|
+
var NotificationSchema = z.object({
|
|
345
|
+
id: z.string(),
|
|
346
|
+
type: z.string(),
|
|
347
|
+
refId: z.string().nullable(),
|
|
348
|
+
refType: z.string().nullable(),
|
|
349
|
+
data: z.record(z.string(), z.unknown()).nullable(),
|
|
350
|
+
read: z.boolean(),
|
|
351
|
+
readAt: z.string().nullable(),
|
|
352
|
+
createdAt: z.string()
|
|
353
|
+
});
|
|
354
|
+
var NotificationListResponseSchema = z.object({
|
|
355
|
+
notifications: z.array(NotificationSchema),
|
|
356
|
+
unreadCount: z.number()
|
|
357
|
+
});
|
|
358
|
+
var SecretCommentSchema = z.object({
|
|
359
|
+
id: z.string(),
|
|
360
|
+
secretId: z.string(),
|
|
361
|
+
authorId: z.string().nullable(),
|
|
362
|
+
authorName: z.string().nullable(),
|
|
363
|
+
authorEmail: z.string().nullable(),
|
|
364
|
+
body: z.string(),
|
|
365
|
+
createdAt: z.string(),
|
|
366
|
+
/** null until the comment has been edited at least once */
|
|
367
|
+
editedAt: z.string().nullable()
|
|
368
|
+
});
|
|
369
|
+
var SecretCommentBodySchema = z.string().trim().min(1, "comment cannot be empty").max(4e3, "comment must be 4000 characters or fewer");
|
|
370
|
+
var CreateSecretCommentRequestSchema = z.object({
|
|
371
|
+
body: SecretCommentBodySchema
|
|
372
|
+
});
|
|
373
|
+
var UpdateSecretCommentRequestSchema = z.object({
|
|
374
|
+
body: SecretCommentBodySchema
|
|
375
|
+
});
|
|
318
376
|
var RegisterRequestSchema = z.object({
|
|
319
377
|
email: z.string().email(),
|
|
320
378
|
password: z.string().min(8),
|
|
@@ -377,11 +435,17 @@ var SetSecretRequestSchema = z.object({
|
|
|
377
435
|
expiresAt: z.string().datetime().nullable().optional(),
|
|
378
436
|
/** omit to leave unchanged, null to clear, a positive integer (days) to set (see
|
|
379
437
|
* "Rotation reminders" in the spec) */
|
|
380
|
-
rotationReminderIntervalDays: z.number().int().positive().nullable().optional()
|
|
438
|
+
rotationReminderIntervalDays: z.number().int().positive().nullable().optional(),
|
|
439
|
+
/** omit to leave tags unchanged, an array (possibly empty, to clear all tags) to replace
|
|
440
|
+
* the full tag set — normalized/deduped by SecretTagsSchema (see "Tags on secrets"). */
|
|
441
|
+
tags: SecretTagsSchema.optional()
|
|
381
442
|
});
|
|
382
443
|
var UpdateSecretMetadataRequestSchema = z.object({
|
|
383
444
|
expiresAt: z.string().datetime().nullable().optional(),
|
|
384
|
-
rotationReminderIntervalDays: z.number().int().positive().nullable().optional()
|
|
445
|
+
rotationReminderIntervalDays: z.number().int().positive().nullable().optional(),
|
|
446
|
+
/** omit to leave tags unchanged, an array (possibly empty, to clear all tags) to replace
|
|
447
|
+
* the full tag set. */
|
|
448
|
+
tags: SecretTagsSchema.optional()
|
|
385
449
|
});
|
|
386
450
|
var RenameSecretRequestSchema = z.object({
|
|
387
451
|
newKey: SecretKeySchema
|
|
@@ -558,6 +622,20 @@ var SecretsHealthSchema = z.object({
|
|
|
558
622
|
expiredReminders: z.array(ExpiredReminderSchema),
|
|
559
623
|
inactiveConfigs: z.array(InactiveConfigSchema)
|
|
560
624
|
});
|
|
625
|
+
var DuplicateValueMemberSchema = SecretsHealthLocationSchema.extend({
|
|
626
|
+
key: z.string()
|
|
627
|
+
});
|
|
628
|
+
var DuplicateValueGroupSchema = z.object({
|
|
629
|
+
count: z.number().int(),
|
|
630
|
+
members: z.array(DuplicateValueMemberSchema)
|
|
631
|
+
});
|
|
632
|
+
var DuplicateValuesResponseSchema = z.object({
|
|
633
|
+
groups: z.array(DuplicateValueGroupSchema),
|
|
634
|
+
/** true if at least one visible secret's current version hasn't been HMAC-indexed yet
|
|
635
|
+
* (null valueHmac) — e.g. a version written before this feature existed. Lets the UI show
|
|
636
|
+
* a "results may be incomplete" notice instead of silently under-reporting duplicates. */
|
|
637
|
+
incomplete: z.boolean()
|
|
638
|
+
});
|
|
561
639
|
|
|
562
640
|
// src/lib/project-config.ts
|
|
563
641
|
var CONFIG_FILE = ".vaultic.yaml";
|
|
@@ -724,6 +802,88 @@ function exitCancelled() {
|
|
|
724
802
|
|
|
725
803
|
// src/commands/auth.ts
|
|
726
804
|
import prompts2 from "prompts";
|
|
805
|
+
|
|
806
|
+
// src/lib/browser-login.ts
|
|
807
|
+
import { createServer } from "http";
|
|
808
|
+
import { randomBytes } from "crypto";
|
|
809
|
+
import { spawn } from "child_process";
|
|
810
|
+
function openInBrowser(url) {
|
|
811
|
+
try {
|
|
812
|
+
const platform = process.platform;
|
|
813
|
+
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
814
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
819
|
+
async function browserLogin(opts) {
|
|
820
|
+
const { serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS, onUrl } = opts;
|
|
821
|
+
const providers = await fetch(`${serverUrl}/auth/providers`).then((res) => res.ok ? res.json() : null).catch(() => null);
|
|
822
|
+
const webAppUrl = providers?.webAppUrl;
|
|
823
|
+
if (!webAppUrl) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
"This server doesn't support browser login (no webAppUrl from /auth/providers) -- use --email/--password or --token instead."
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const state = randomBytes(24).toString("hex");
|
|
829
|
+
return new Promise((resolve, reject) => {
|
|
830
|
+
let settled = false;
|
|
831
|
+
const timeout = setTimeout(() => {
|
|
832
|
+
if (settled) return;
|
|
833
|
+
settled = true;
|
|
834
|
+
server.close();
|
|
835
|
+
reject(new Error("Timed out waiting for browser login approval."));
|
|
836
|
+
}, timeoutMs);
|
|
837
|
+
const server = createServer((req, res) => {
|
|
838
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
839
|
+
if (url.pathname !== "/callback") {
|
|
840
|
+
res.writeHead(404).end();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const returnedState = url.searchParams.get("state");
|
|
844
|
+
const code = url.searchParams.get("code");
|
|
845
|
+
const finish = (err, result) => {
|
|
846
|
+
if (settled) return;
|
|
847
|
+
settled = true;
|
|
848
|
+
clearTimeout(timeout);
|
|
849
|
+
server.close();
|
|
850
|
+
if (err) reject(err);
|
|
851
|
+
else resolve(result);
|
|
852
|
+
};
|
|
853
|
+
if (returnedState !== state || !code) {
|
|
854
|
+
res.writeHead(400, { "content-type": "text/html" }).end("<h1>Invalid login attempt.</h1>You can close this tab.");
|
|
855
|
+
finish(new Error("Received an invalid or mismatched login callback."));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
fetch(`${serverUrl}/auth/cli/exchange`, {
|
|
859
|
+
method: "POST",
|
|
860
|
+
headers: { "content-type": "application/json" },
|
|
861
|
+
body: JSON.stringify({ code })
|
|
862
|
+
}).then(async (exchangeRes) => {
|
|
863
|
+
const data = await exchangeRes.json();
|
|
864
|
+
if (!exchangeRes.ok) throw new Error(data?.error ?? "Login failed");
|
|
865
|
+
res.writeHead(200, { "content-type": "text/html" }).end("<h1>Logged in!</h1>You can close this tab and return to your terminal.");
|
|
866
|
+
finish(null, data);
|
|
867
|
+
}).catch((err) => {
|
|
868
|
+
res.writeHead(500, { "content-type": "text/html" }).end("<h1>Login failed.</h1>You can close this tab.");
|
|
869
|
+
finish(err instanceof Error ? err : new Error(String(err)));
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
server.listen(0, "127.0.0.1", () => {
|
|
873
|
+
const port = server.address().port;
|
|
874
|
+
const loginUrl = `${webAppUrl}/cli-login?state=${state}&port=${port}`;
|
|
875
|
+
if (onUrl) {
|
|
876
|
+
onUrl(loginUrl);
|
|
877
|
+
} else {
|
|
878
|
+
console.log(`Opening ${loginUrl} in your browser\u2026`);
|
|
879
|
+
console.log("Waiting for you to approve the login (Ctrl+C to cancel)\u2026");
|
|
880
|
+
openInBrowser(loginUrl);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/commands/auth.ts
|
|
727
887
|
async function loginCommand(opts) {
|
|
728
888
|
const serverUrl = opts.server ?? process.env.VAULTIC_API_URL ?? DEFAULT_SERVER_URL;
|
|
729
889
|
if (opts.token) {
|
|
@@ -731,6 +891,12 @@ async function loginCommand(opts) {
|
|
|
731
891
|
console.log(`Saved credentials for ${serverUrl}`);
|
|
732
892
|
return;
|
|
733
893
|
}
|
|
894
|
+
if (!opts.email && !opts.password && opts.browser !== false) {
|
|
895
|
+
const res = await browserLogin({ serverUrl });
|
|
896
|
+
saveCredentials({ serverUrl, token: res.token, email: res.user.email });
|
|
897
|
+
console.log(`Logged in as ${res.user.email}`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
734
900
|
const email = opts.email ?? (await prompts2({ type: "text", name: "email", message: "Email" })).email;
|
|
735
901
|
const password = opts.password ?? (await prompts2({ type: "password", name: "password", message: "Password" })).password;
|
|
736
902
|
if (!email || !password) {
|
|
@@ -766,7 +932,7 @@ async function whoamiCommand() {
|
|
|
766
932
|
console.log("Not logged in.");
|
|
767
933
|
return;
|
|
768
934
|
}
|
|
769
|
-
if (creds.token.startsWith("
|
|
935
|
+
if (creds.token.startsWith("sht_")) {
|
|
770
936
|
console.log(`Authenticated with a service token against ${creds.serverUrl}`);
|
|
771
937
|
return;
|
|
772
938
|
}
|
|
@@ -776,7 +942,7 @@ async function whoamiCommand() {
|
|
|
776
942
|
}
|
|
777
943
|
|
|
778
944
|
// src/commands/run.ts
|
|
779
|
-
import { spawn } from "child_process";
|
|
945
|
+
import { spawn as spawn2 } from "child_process";
|
|
780
946
|
|
|
781
947
|
// src/lib/context.ts
|
|
782
948
|
function envBase(cfg, env2) {
|
|
@@ -802,7 +968,7 @@ async function runCommand(commandParts, opts) {
|
|
|
802
968
|
const fetchSecrets = () => api.get(`${envBase(cfg, env2)}/export`);
|
|
803
969
|
if (!opts.watch) {
|
|
804
970
|
const secrets2 = await fetchSecrets();
|
|
805
|
-
const child2 =
|
|
971
|
+
const child2 = spawn2(cmd, args, { stdio: "inherit", env: { ...process.env, ...secrets2 } });
|
|
806
972
|
child2.on("exit", (code, signal) => {
|
|
807
973
|
if (signal) process.kill(process.pid, signal);
|
|
808
974
|
else process.exit(code ?? 0);
|
|
@@ -819,7 +985,7 @@ async function runCommand(commandParts, opts) {
|
|
|
819
985
|
let stopped = false;
|
|
820
986
|
let restarting = false;
|
|
821
987
|
function spawnChild() {
|
|
822
|
-
child =
|
|
988
|
+
child = spawn2(cmd, args, { stdio: "inherit", env: { ...process.env, ...current } });
|
|
823
989
|
child.on("exit", (code, signal) => {
|
|
824
990
|
if (stopped || restarting) return;
|
|
825
991
|
stopped = true;
|
|
@@ -1094,89 +1260,23 @@ async function syncCommand(opts) {
|
|
|
1094
1260
|
import readline from "readline";
|
|
1095
1261
|
|
|
1096
1262
|
// src/rotation/postgres.ts
|
|
1097
|
-
import { randomBytes } from "crypto";
|
|
1098
|
-
import { Client } from "pg";
|
|
1099
|
-
var ROLE_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1100
|
-
function generatePassword(bytes = 24) {
|
|
1101
|
-
return randomBytes(bytes).toString("base64url");
|
|
1102
|
-
}
|
|
1103
|
-
function sqlLiteral(value) {
|
|
1104
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
1105
|
-
}
|
|
1106
1263
|
var postgresRotationPlugin = {
|
|
1107
1264
|
name: "postgres",
|
|
1108
1265
|
describe() {
|
|
1109
|
-
return "Rotates a PostgreSQL role's password via `ALTER ROLE ... WITH PASSWORD
|
|
1266
|
+
return "Rotates a PostgreSQL role's password via `ALTER ROLE ... WITH PASSWORD ...`, executed server-side (the CLI never connects to the database directly). Treats the secret's current value as a postgres://user:password@host:port/db connection string. Options: --role <name> (defaults to the connection string's user), --admin-conn-string <url> (connect with different credentials than the value being rotated, e.g. a superuser, when the target role can't alter its own password), --new-password <value> (defaults to a random 32-character password).";
|
|
1110
1267
|
},
|
|
1111
1268
|
async rotate(ctx) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
if (targetUrl.protocol !== "postgres:" && targetUrl.protocol !== "postgresql:") {
|
|
1121
|
-
throw new Error(
|
|
1122
|
-
`Current value for "${ctx.key}" has scheme "${targetUrl.protocol}", expected postgres:// or postgresql://.`
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
const role = ctx.options.role ?? decodeURIComponent(targetUrl.username);
|
|
1126
|
-
if (!role) throw new Error(`Could not determine the role to rotate for "${ctx.key}"; pass --role explicitly.`);
|
|
1127
|
-
if (!ROLE_NAME_REGEX.test(role)) {
|
|
1128
|
-
throw new Error(`Refusing to rotate role "${role}": expected [A-Za-z_][A-Za-z0-9_]* (use --role to override).`);
|
|
1129
|
-
}
|
|
1130
|
-
const connectionString = ctx.options.adminConnString ?? ctx.currentValue;
|
|
1131
|
-
const newPassword = ctx.options.newPassword ?? generatePassword();
|
|
1132
|
-
const client = new Client({ connectionString });
|
|
1133
|
-
await client.connect();
|
|
1134
|
-
try {
|
|
1135
|
-
await client.query(`ALTER ROLE "${role}" WITH PASSWORD ${sqlLiteral(newPassword)}`);
|
|
1136
|
-
} finally {
|
|
1137
|
-
await client.end();
|
|
1138
|
-
}
|
|
1139
|
-
const newUrl = new URL(targetUrl.toString());
|
|
1140
|
-
newUrl.password = newPassword;
|
|
1141
|
-
return {
|
|
1142
|
-
newValue: newUrl.toString(),
|
|
1143
|
-
summary: `Rotated PostgreSQL role "${role}"'s password (ALTER ROLE) on ${targetUrl.hostname}:${targetUrl.port || "5432"}/${targetUrl.pathname.replace(/^\//, "")}`
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
// src/rotation/types.ts
|
|
1149
|
-
var RotationNotImplementedError = class extends Error {
|
|
1150
|
-
};
|
|
1151
|
-
|
|
1152
|
-
// src/rotation/stripe.ts
|
|
1153
|
-
var stripeRotationPlugin = {
|
|
1154
|
-
name: "stripe",
|
|
1155
|
-
describe() {
|
|
1156
|
-
return "STUB \u2014 not exercised against a live Stripe account in this environment. Documents the interface for restricted-key rotation (create new key with same scopes, store it, revoke the old one after the grace window). See packages/cli/src/rotation/stripe.ts for what a real implementation needs.";
|
|
1157
|
-
},
|
|
1158
|
-
async rotate() {
|
|
1159
|
-
throw new RotationNotImplementedError(
|
|
1160
|
-
"The stripe rotation plugin is a documented stub (no Stripe credentials available in this environment to build/verify a real integration) \u2014 see `packages/cli/src/rotation/stripe.ts`."
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
// src/rotation/aws.ts
|
|
1166
|
-
var awsIamRotationPlugin = {
|
|
1167
|
-
name: "aws-iam",
|
|
1168
|
-
describe() {
|
|
1169
|
-
return "STUB \u2014 not exercised against a live AWS account in this environment. Documents the interface for IAM access-key rotation (CreateAccessKey for a new key while the old one stays active, then deactivate/delete the old key after the grace window). See packages/cli/src/rotation/aws.ts for what a real implementation needs.";
|
|
1170
|
-
},
|
|
1171
|
-
async rotate() {
|
|
1172
|
-
throw new RotationNotImplementedError(
|
|
1173
|
-
"The aws-iam rotation plugin is a documented stub (no AWS credentials available in this environment to build/verify a real integration) \u2014 see `packages/cli/src/rotation/aws.ts`."
|
|
1174
|
-
);
|
|
1269
|
+
const response = await ctx.api.post(`${ctx.envBase}/secrets/${ctx.key}/rotate/execute`, {
|
|
1270
|
+
provider: "postgres",
|
|
1271
|
+
currentValue: ctx.currentValue,
|
|
1272
|
+
options: ctx.options
|
|
1273
|
+
});
|
|
1274
|
+
return { newValue: response.newValue, summary: response.summary };
|
|
1175
1275
|
}
|
|
1176
1276
|
};
|
|
1177
1277
|
|
|
1178
1278
|
// src/rotation/index.ts
|
|
1179
|
-
var PLUGINS = [postgresRotationPlugin
|
|
1279
|
+
var PLUGINS = [postgresRotationPlugin];
|
|
1180
1280
|
function getRotationPlugin(name) {
|
|
1181
1281
|
const plugin = PLUGINS.find((p) => p.name === name);
|
|
1182
1282
|
if (!plugin) {
|
|
@@ -1294,7 +1394,9 @@ async function secretsRotateCommand(key, value, opts) {
|
|
|
1294
1394
|
const result = await plugin.rotate({
|
|
1295
1395
|
key,
|
|
1296
1396
|
currentValue: current.value,
|
|
1297
|
-
options: { role: opts.role, adminConnString: opts.adminConnString, newPassword: opts.newPassword }
|
|
1397
|
+
options: { role: opts.role, adminConnString: opts.adminConnString, newPassword: opts.newPassword },
|
|
1398
|
+
api,
|
|
1399
|
+
envBase: envBase(cfg, env2)
|
|
1298
1400
|
});
|
|
1299
1401
|
console.log(result.summary);
|
|
1300
1402
|
resolvedValue = result.newValue;
|
|
@@ -1956,7 +2058,7 @@ async function gitIntegrationRemoveCommand() {
|
|
|
1956
2058
|
var program = new Command();
|
|
1957
2059
|
program.name("vaultic").description("Vaultic CLI").version("0.1.0");
|
|
1958
2060
|
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
|
|
2061
|
+
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
2062
|
program.command("logout").description("Clear stored credentials").action(wrap(logoutCommand));
|
|
1961
2063
|
program.command("whoami").description("Show the current logged-in user").action(wrap(whoamiCommand));
|
|
1962
2064
|
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)));
|
|
@@ -1972,7 +2074,7 @@ secrets.command("delete").argument("<key>").option("-e, --env <environment>").ac
|
|
|
1972
2074
|
secrets.command("history").argument("<key>").option("-e, --env <environment>").option("--reveal").action(wrap(secretsHistoryCommand));
|
|
1973
2075
|
secrets.command("rollback").argument("<key>").requiredOption("--version <version>").option("-e, --env <environment>").action(wrap(secretsRollbackCommand));
|
|
1974
2076
|
secrets.command("rename").argument("<oldKey>").argument("<newKey>").option("-e, --env <environment>").action(wrap(secretsRenameCommand));
|
|
1975
|
-
secrets.command("rotate").description("Set a new value, keeping the old one fetchable via 'secrets get KEY --previous'").argument("<key>").argument("[value]", "new value, or '-' to read from stdin (omit entirely when using --provider)").option("-e, --env <environment>").option("--from-file <path>", "read the new value from a file").option("--grace <duration>", 'how long the previous value stays fetchable, e.g. "1h", "30m" (omit for no limit)').option("--provider <name>", "have a rotation plugin compute the new value (postgres
|
|
2077
|
+
secrets.command("rotate").description("Set a new value, keeping the old one fetchable via 'secrets get KEY --previous'").argument("<key>").argument("[value]", "new value, or '-' to read from stdin (omit entirely when using --provider)").option("-e, --env <environment>").option("--from-file <path>", "read the new value from a file").option("--grace <duration>", 'how long the previous value stays fetchable, e.g. "1h", "30m" (omit for no limit)').option("--provider <name>", "have a rotation plugin compute the new value (postgres)").option("--role <role>", "[postgres] role to rotate (defaults to the connection string's user)").option("--admin-conn-string <url>", "[postgres] connect with different credentials than the value being rotated").option("--new-password <password>", "[postgres] use this password instead of a random one").action(wrap(secretsRotateCommand));
|
|
1976
2078
|
program.command("rotation-providers").description("List available `secrets rotate --provider` plugins").action(wrap(rotationProvidersCommand));
|
|
1977
2079
|
var override = secrets.command("override").description("Manage your personal, server-side value overrides");
|
|
1978
2080
|
override.command("set").argument("<key>").argument("[value]", "value, or '-' to read from stdin").option("-e, --env <environment>").option("--from-file <path>", "read the value from a file").action(wrap(secretsOverrideSetCommand));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaultic-dev/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vaultic": "./dist/index.js"
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=20"
|
|
13
13
|
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/vaultic-dev/vaultic.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/vaultic-dev/vaultic#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/vaultic-dev/vaultic/issues"
|
|
22
|
+
},
|
|
14
23
|
"publishConfig": {
|
|
15
24
|
"access": "public"
|
|
16
25
|
},
|
|
@@ -26,7 +35,6 @@
|
|
|
26
35
|
"commander": "^12.1.0",
|
|
27
36
|
"js-yaml": "^4.1.0",
|
|
28
37
|
"libsodium-wrappers": "^0.8.4",
|
|
29
|
-
"pg": "^8.13.1",
|
|
30
38
|
"prompts": "^2.4.2",
|
|
31
39
|
"zod": "^3.23.8"
|
|
32
40
|
},
|
|
@@ -34,7 +42,6 @@
|
|
|
34
42
|
"@types/js-yaml": "^4.0.9",
|
|
35
43
|
"@types/libsodium-wrappers": "^0.8.2",
|
|
36
44
|
"@types/node": "^22.7.5",
|
|
37
|
-
"@types/pg": "^8.11.10",
|
|
38
45
|
"@types/prompts": "^2.4.9",
|
|
39
46
|
"tsup": "^8.3.0",
|
|
40
47
|
"tsx": "^4.19.1",
|