@vaultic-dev/cli 0.1.0
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/index.js +2034 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2034 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
|
|
9
|
+
// src/lib/credentials.ts
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
var CONFIG_DIR = path.join(os.homedir(), ".vaultic");
|
|
14
|
+
var CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
15
|
+
var DEFAULT_SERVER_URL = "https://vaultic.dev/api";
|
|
16
|
+
function loadCredentials() {
|
|
17
|
+
if (!existsSync(CREDENTIALS_PATH)) return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(CREDENTIALS_PATH, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveCredentials(creds) {
|
|
25
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
27
|
+
}
|
|
28
|
+
function clearCredentials() {
|
|
29
|
+
if (existsSync(CREDENTIALS_PATH)) rmSync(CREDENTIALS_PATH);
|
|
30
|
+
}
|
|
31
|
+
function requireCredentials() {
|
|
32
|
+
const creds = loadCredentials();
|
|
33
|
+
if (!creds) {
|
|
34
|
+
console.error("Not logged in. Run `vaultic login` first.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
return creds;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/lib/api.ts
|
|
41
|
+
var ApiError = class extends Error {
|
|
42
|
+
status;
|
|
43
|
+
constructor(status, message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.status = status;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var ApiClient = class {
|
|
49
|
+
serverUrl;
|
|
50
|
+
token;
|
|
51
|
+
constructor(opts = {}) {
|
|
52
|
+
const creds = loadCredentials();
|
|
53
|
+
this.serverUrl = opts.serverUrl ?? creds?.serverUrl ?? process.env.VAULTIC_API_URL ?? DEFAULT_SERVER_URL;
|
|
54
|
+
this.token = opts.token ?? creds?.token ?? "";
|
|
55
|
+
}
|
|
56
|
+
async request(method, urlPath, body) {
|
|
57
|
+
const headers = { "x-vaultic-source": "cli" };
|
|
58
|
+
if (this.token) headers.authorization = `Bearer ${this.token}`;
|
|
59
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
60
|
+
const res = await fetch(`${this.serverUrl}${urlPath}`, {
|
|
61
|
+
method,
|
|
62
|
+
headers,
|
|
63
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
64
|
+
});
|
|
65
|
+
if (res.status === 204) return void 0;
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
const data = text ? JSON.parse(text) : void 0;
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw new ApiError(res.status, data?.error ?? `Request failed with status ${res.status}`);
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
get(urlPath) {
|
|
74
|
+
return this.request("GET", urlPath);
|
|
75
|
+
}
|
|
76
|
+
post(urlPath, body) {
|
|
77
|
+
return this.request("POST", urlPath, body ?? {});
|
|
78
|
+
}
|
|
79
|
+
put(urlPath, body) {
|
|
80
|
+
return this.request("PUT", urlPath, body ?? {});
|
|
81
|
+
}
|
|
82
|
+
patch(urlPath, body) {
|
|
83
|
+
return this.request("PATCH", urlPath, body ?? {});
|
|
84
|
+
}
|
|
85
|
+
delete(urlPath) {
|
|
86
|
+
return this.request("DELETE", urlPath);
|
|
87
|
+
}
|
|
88
|
+
getServerUrl() {
|
|
89
|
+
return this.serverUrl;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/lib/project-config.ts
|
|
94
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
95
|
+
import path2 from "path";
|
|
96
|
+
import yaml from "js-yaml";
|
|
97
|
+
|
|
98
|
+
// ../shared/dist/index.js
|
|
99
|
+
import { z } from "zod";
|
|
100
|
+
var SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
101
|
+
var SlugSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric with dashes");
|
|
102
|
+
var SecretKeySchema = z.string().min(1).max(256).regex(SECRET_KEY_REGEX, "keys must be UPPER_SNAKE_CASE (e.g. DATABASE_URL)");
|
|
103
|
+
var RoleSchema = z.enum(["owner", "admin", "developer", "read-only"]);
|
|
104
|
+
var TokenAccessSchema = z.enum(["read", "write"]);
|
|
105
|
+
var SecretTypeHintSchema = z.enum([
|
|
106
|
+
"string",
|
|
107
|
+
"multiline",
|
|
108
|
+
"json",
|
|
109
|
+
"url",
|
|
110
|
+
"boolean",
|
|
111
|
+
"number",
|
|
112
|
+
"certificate",
|
|
113
|
+
"private-key"
|
|
114
|
+
]);
|
|
115
|
+
var WorkspaceSchema = z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
slug: SlugSchema,
|
|
118
|
+
name: z.string(),
|
|
119
|
+
createdAt: z.string()
|
|
120
|
+
});
|
|
121
|
+
var ProjectSchema = z.object({
|
|
122
|
+
id: z.string(),
|
|
123
|
+
workspaceId: z.string(),
|
|
124
|
+
slug: SlugSchema,
|
|
125
|
+
name: z.string(),
|
|
126
|
+
createdAt: z.string()
|
|
127
|
+
});
|
|
128
|
+
var EnvironmentSchema = z.object({
|
|
129
|
+
id: z.string(),
|
|
130
|
+
projectId: z.string(),
|
|
131
|
+
slug: SlugSchema,
|
|
132
|
+
name: z.string(),
|
|
133
|
+
parentId: z.string().nullable(),
|
|
134
|
+
parentSlug: z.string().nullable().optional(),
|
|
135
|
+
lockedAt: z.string().nullable(),
|
|
136
|
+
createdAt: z.string(),
|
|
137
|
+
/** ephemeral/preview environments auto-expire — see Rotation/Environments in the spec */
|
|
138
|
+
isEphemeral: z.boolean().default(false),
|
|
139
|
+
expiresAt: z.string().nullable().default(null),
|
|
140
|
+
sourceBranch: z.string().nullable().optional(),
|
|
141
|
+
/** live count of non-deleted secrets defined directly in this environment; only populated
|
|
142
|
+
* by the environments-list endpoint (cheap to compute there), not on single-environment reads */
|
|
143
|
+
secretCount: z.number().int().optional()
|
|
144
|
+
});
|
|
145
|
+
var WorkspaceMemberSchema = z.object({
|
|
146
|
+
id: z.string(),
|
|
147
|
+
workspaceId: z.string(),
|
|
148
|
+
userId: z.string(),
|
|
149
|
+
email: z.string(),
|
|
150
|
+
name: z.string(),
|
|
151
|
+
role: RoleSchema,
|
|
152
|
+
createdAt: z.string()
|
|
153
|
+
});
|
|
154
|
+
var InviteRoleSchema = z.enum(["admin", "developer", "read-only"]);
|
|
155
|
+
var CreateInviteRequestSchema = z.object({
|
|
156
|
+
email: z.string().email(),
|
|
157
|
+
role: InviteRoleSchema
|
|
158
|
+
});
|
|
159
|
+
var WorkspaceInviteSchema = z.object({
|
|
160
|
+
id: z.string(),
|
|
161
|
+
workspaceId: z.string(),
|
|
162
|
+
email: z.string(),
|
|
163
|
+
role: RoleSchema,
|
|
164
|
+
status: z.enum(["pending", "accepted", "revoked"]),
|
|
165
|
+
invitedByEmail: z.string().nullable(),
|
|
166
|
+
createdAt: z.string(),
|
|
167
|
+
expiresAt: z.string().nullable()
|
|
168
|
+
});
|
|
169
|
+
var WorkspaceInviteCreatedSchema = WorkspaceInviteSchema.extend({
|
|
170
|
+
token: z.string()
|
|
171
|
+
});
|
|
172
|
+
var PublicInviteSchema = z.object({
|
|
173
|
+
workspaceSlug: z.string(),
|
|
174
|
+
workspaceName: z.string(),
|
|
175
|
+
email: z.string(),
|
|
176
|
+
role: RoleSchema,
|
|
177
|
+
expiresAt: z.string().nullable()
|
|
178
|
+
});
|
|
179
|
+
var ChangeMemberRoleRequestSchema = z.object({
|
|
180
|
+
role: RoleSchema
|
|
181
|
+
});
|
|
182
|
+
var GrantRoleSchema = z.enum(["developer", "read-only"]);
|
|
183
|
+
var CreateAccessGrantRequestSchema = z.object({
|
|
184
|
+
memberId: z.string(),
|
|
185
|
+
/** omit for a project-wide grant; set to scope the grant to one environment */
|
|
186
|
+
environmentSlug: z.string().optional(),
|
|
187
|
+
role: GrantRoleSchema
|
|
188
|
+
});
|
|
189
|
+
var AccessGrantSchema = z.object({
|
|
190
|
+
id: z.string(),
|
|
191
|
+
memberId: z.string(),
|
|
192
|
+
memberEmail: z.string(),
|
|
193
|
+
projectId: z.string(),
|
|
194
|
+
environmentId: z.string().nullable(),
|
|
195
|
+
environmentSlug: z.string().nullable(),
|
|
196
|
+
role: GrantRoleSchema,
|
|
197
|
+
createdAt: z.string()
|
|
198
|
+
});
|
|
199
|
+
var SecretSummarySchema = z.object({
|
|
200
|
+
id: z.string(),
|
|
201
|
+
environmentId: z.string(),
|
|
202
|
+
key: z.string(),
|
|
203
|
+
maskedValue: z.string(),
|
|
204
|
+
// e.g. "••••••••"
|
|
205
|
+
typeHint: SecretTypeHintSchema,
|
|
206
|
+
note: z.string().nullable(),
|
|
207
|
+
version: z.number().int(),
|
|
208
|
+
createdBy: z.string().nullable(),
|
|
209
|
+
updatedAt: z.string(),
|
|
210
|
+
createdAt: z.string(),
|
|
211
|
+
/** set when this key isn't defined directly in the requested environment but falls
|
|
212
|
+
* through from a parent via inheritance */
|
|
213
|
+
inheritedFrom: z.string().nullable().optional(),
|
|
214
|
+
/** set when this key's value is `${...}` reference syntax rather than a literal */
|
|
215
|
+
isReference: z.boolean().optional(),
|
|
216
|
+
/** set when the current user has a personal override active for this key */
|
|
217
|
+
hasPersonalOverride: z.boolean().optional(),
|
|
218
|
+
/** set when this secret has a previous version fetchable via `--previous` (see Rotation) */
|
|
219
|
+
hasPreviousVersion: z.boolean().optional(),
|
|
220
|
+
/** ISO timestamp after which `--previous` stops serving the pre-rotation value; null means
|
|
221
|
+
* no time limit was set on the grace window */
|
|
222
|
+
rotationGraceUntil: z.string().nullable().optional(),
|
|
223
|
+
/** optional expiry date (spec: "Expiring secrets") — warnings only, never enforced */
|
|
224
|
+
expiresAt: z.string().nullable().optional(),
|
|
225
|
+
/** optional rotation-reminder cadence in days (spec: "Rotation reminders per secret") */
|
|
226
|
+
rotationReminderIntervalDays: z.number().int().nullable().optional(),
|
|
227
|
+
/** when this secret's value was last changed — `rotatedAt` if it's ever gone through
|
|
228
|
+
* `rotate`, otherwise the most recent version's createdAt. Used to compute rotation-due
|
|
229
|
+
* state (see `isRotationDue`) client-side, in the CLI, and by the scheduled lifecycle job. */
|
|
230
|
+
lastRotatedAt: z.string().nullable().optional()
|
|
231
|
+
});
|
|
232
|
+
var SecretRevealedSchema = SecretSummarySchema.extend({
|
|
233
|
+
value: z.string()
|
|
234
|
+
}).omit({ maskedValue: true });
|
|
235
|
+
var SecretVersionSchema = z.object({
|
|
236
|
+
id: z.string(),
|
|
237
|
+
secretId: z.string(),
|
|
238
|
+
version: z.number().int(),
|
|
239
|
+
value: z.string().optional(),
|
|
240
|
+
// only present when explicitly revealed
|
|
241
|
+
createdBy: z.string().nullable(),
|
|
242
|
+
createdAt: z.string(),
|
|
243
|
+
changeType: z.enum(["create", "update", "rollback", "rename", "delete", "rotate"])
|
|
244
|
+
});
|
|
245
|
+
var TrashedSecretSchema = z.object({
|
|
246
|
+
id: z.string(),
|
|
247
|
+
environmentId: z.string(),
|
|
248
|
+
key: z.string(),
|
|
249
|
+
typeHint: SecretTypeHintSchema,
|
|
250
|
+
note: z.string().nullable(),
|
|
251
|
+
version: z.number().int(),
|
|
252
|
+
deletedAt: z.string(),
|
|
253
|
+
/** ISO timestamp after which the background job hard-deletes this secret for good */
|
|
254
|
+
purgesAt: z.string()
|
|
255
|
+
});
|
|
256
|
+
var RotateSecretRequestSchema = z.object({
|
|
257
|
+
value: z.string(),
|
|
258
|
+
/** e.g. "1h", "30m", "2d" — how long the pre-rotation value stays fetchable via
|
|
259
|
+
* `secrets get KEY --previous`. Omit for no time limit. */
|
|
260
|
+
graceWindow: z.string().optional()
|
|
261
|
+
});
|
|
262
|
+
function parseDurationMs(spec) {
|
|
263
|
+
const match = /^(\d+)(s|m|h|d)$/.exec(spec.trim());
|
|
264
|
+
if (!match)
|
|
265
|
+
throw new Error(`Invalid duration "${spec}" (expected e.g. "30m", "1h", "2d")`);
|
|
266
|
+
const value = Number(match[1]);
|
|
267
|
+
const unitMs = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[match[2]];
|
|
268
|
+
return value * unitMs;
|
|
269
|
+
}
|
|
270
|
+
var DEFAULT_SECRET_EXPIRY_WARNING_DAYS = 7;
|
|
271
|
+
function secretExpiryState(expiresAt, now = /* @__PURE__ */ new Date(), warningDays = DEFAULT_SECRET_EXPIRY_WARNING_DAYS) {
|
|
272
|
+
if (!expiresAt)
|
|
273
|
+
return null;
|
|
274
|
+
const expiresAtMs = new Date(expiresAt).getTime();
|
|
275
|
+
const nowMs = now.getTime();
|
|
276
|
+
if (expiresAtMs <= nowMs)
|
|
277
|
+
return "expired";
|
|
278
|
+
if (expiresAtMs - nowMs <= warningDays * 864e5)
|
|
279
|
+
return "expiring";
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
function isRotationDue(rotationReminderIntervalDays, lastRotatedAt, now = /* @__PURE__ */ new Date()) {
|
|
283
|
+
if (!rotationReminderIntervalDays || !lastRotatedAt)
|
|
284
|
+
return false;
|
|
285
|
+
const lastRotatedMs = new Date(lastRotatedAt).getTime();
|
|
286
|
+
return now.getTime() - lastRotatedMs >= rotationReminderIntervalDays * 864e5;
|
|
287
|
+
}
|
|
288
|
+
var ServiceTokenSchema = z.object({
|
|
289
|
+
id: z.string(),
|
|
290
|
+
name: z.string(),
|
|
291
|
+
workspaceId: z.string(),
|
|
292
|
+
projectId: z.string().nullable(),
|
|
293
|
+
environmentId: z.string().nullable(),
|
|
294
|
+
access: TokenAccessSchema,
|
|
295
|
+
expiresAt: z.string().nullable(),
|
|
296
|
+
createdAt: z.string(),
|
|
297
|
+
lastUsedAt: z.string().nullable(),
|
|
298
|
+
revokedAt: z.string().nullable()
|
|
299
|
+
});
|
|
300
|
+
var ServiceTokenCreatedSchema = ServiceTokenSchema.extend({
|
|
301
|
+
token: z.string()
|
|
302
|
+
});
|
|
303
|
+
var AuditLogEntrySchema = z.object({
|
|
304
|
+
id: z.string(),
|
|
305
|
+
workspaceId: z.string(),
|
|
306
|
+
projectId: z.string().nullable(),
|
|
307
|
+
environmentId: z.string().nullable(),
|
|
308
|
+
actorType: z.enum(["user", "token"]),
|
|
309
|
+
actorId: z.string().nullable(),
|
|
310
|
+
actorLabel: z.string(),
|
|
311
|
+
action: z.string(),
|
|
312
|
+
// e.g. "secret.created", "secret.revealed"
|
|
313
|
+
targetKey: z.string().nullable(),
|
|
314
|
+
source: z.enum(["ui", "cli", "api", "token"]),
|
|
315
|
+
ip: z.string().nullable(),
|
|
316
|
+
createdAt: z.string()
|
|
317
|
+
});
|
|
318
|
+
var RegisterRequestSchema = z.object({
|
|
319
|
+
email: z.string().email(),
|
|
320
|
+
password: z.string().min(8),
|
|
321
|
+
name: z.string().min(1).max(128)
|
|
322
|
+
});
|
|
323
|
+
var LoginRequestSchema = z.object({
|
|
324
|
+
email: z.string().email(),
|
|
325
|
+
password: z.string().min(1)
|
|
326
|
+
});
|
|
327
|
+
var AuthResponseSchema = z.object({
|
|
328
|
+
token: z.string(),
|
|
329
|
+
user: z.object({
|
|
330
|
+
id: z.string(),
|
|
331
|
+
email: z.string(),
|
|
332
|
+
name: z.string()
|
|
333
|
+
})
|
|
334
|
+
});
|
|
335
|
+
var CreateWorkspaceRequestSchema = z.object({
|
|
336
|
+
slug: SlugSchema,
|
|
337
|
+
name: z.string().min(1)
|
|
338
|
+
});
|
|
339
|
+
var CreateProjectRequestSchema = z.object({
|
|
340
|
+
slug: SlugSchema,
|
|
341
|
+
name: z.string().min(1),
|
|
342
|
+
/** defaults to development/staging/production if omitted */
|
|
343
|
+
environments: z.array(z.string()).optional()
|
|
344
|
+
});
|
|
345
|
+
var CreateEnvironmentRequestSchema = z.object({
|
|
346
|
+
slug: SlugSchema,
|
|
347
|
+
name: z.string().min(1).optional(),
|
|
348
|
+
/** slug of an existing environment in the same project to inherit unset keys from */
|
|
349
|
+
inherits: z.string().optional(),
|
|
350
|
+
/** flags this as an ephemeral/preview environment; pair with ttlDays and/or expiresAt */
|
|
351
|
+
ephemeral: z.boolean().optional(),
|
|
352
|
+
/** convenience: sets expiresAt to now + ttlDays */
|
|
353
|
+
ttlDays: z.number().positive().optional(),
|
|
354
|
+
/** explicit expiry timestamp (ISO), takes precedence over ttlDays if both are given */
|
|
355
|
+
expiresAt: z.string().datetime().optional()
|
|
356
|
+
});
|
|
357
|
+
var DuplicateEnvironmentRequestSchema = z.object({
|
|
358
|
+
targetSlug: SlugSchema,
|
|
359
|
+
mode: z.enum(["values", "keys-only", "link"]).default("values")
|
|
360
|
+
});
|
|
361
|
+
var PromoteEnvironmentRequestSchema = z.object({
|
|
362
|
+
to: z.string().min(1)
|
|
363
|
+
});
|
|
364
|
+
var PromoteEnvironmentResultSchema = z.object({
|
|
365
|
+
promoted: z.array(z.string()),
|
|
366
|
+
created: z.array(z.string()),
|
|
367
|
+
updated: z.array(z.string()),
|
|
368
|
+
targetOnly: z.array(z.string()),
|
|
369
|
+
unchanged: z.array(z.string())
|
|
370
|
+
});
|
|
371
|
+
var SetSecretRequestSchema = z.object({
|
|
372
|
+
value: z.string(),
|
|
373
|
+
typeHint: SecretTypeHintSchema.optional(),
|
|
374
|
+
note: z.string().optional(),
|
|
375
|
+
/** omit to leave unchanged, null to clear, an ISO datetime string to set (see "Expiring
|
|
376
|
+
* secrets" in the spec) */
|
|
377
|
+
expiresAt: z.string().datetime().nullable().optional(),
|
|
378
|
+
/** omit to leave unchanged, null to clear, a positive integer (days) to set (see
|
|
379
|
+
* "Rotation reminders" in the spec) */
|
|
380
|
+
rotationReminderIntervalDays: z.number().int().positive().nullable().optional()
|
|
381
|
+
});
|
|
382
|
+
var UpdateSecretMetadataRequestSchema = z.object({
|
|
383
|
+
expiresAt: z.string().datetime().nullable().optional(),
|
|
384
|
+
rotationReminderIntervalDays: z.number().int().positive().nullable().optional()
|
|
385
|
+
});
|
|
386
|
+
var RenameSecretRequestSchema = z.object({
|
|
387
|
+
newKey: SecretKeySchema
|
|
388
|
+
});
|
|
389
|
+
var SetPersonalOverrideRequestSchema = z.object({
|
|
390
|
+
value: z.string()
|
|
391
|
+
});
|
|
392
|
+
var CreateShareLinkRequestSchema = z.object({
|
|
393
|
+
/** e.g. "1h", "30m", "2d" */
|
|
394
|
+
expiresIn: z.string().optional(),
|
|
395
|
+
maxViews: z.number().int().positive().optional()
|
|
396
|
+
});
|
|
397
|
+
var ShareLinkCreatedSchema = z.object({
|
|
398
|
+
token: z.string(),
|
|
399
|
+
key: z.string(),
|
|
400
|
+
expiresAt: z.string().nullable(),
|
|
401
|
+
maxViews: z.number().nullable()
|
|
402
|
+
});
|
|
403
|
+
var WEBHOOK_EVENTS = [
|
|
404
|
+
"secret.created",
|
|
405
|
+
"secret.updated",
|
|
406
|
+
"secret.deleted",
|
|
407
|
+
"secret.restored",
|
|
408
|
+
"secret.revealed",
|
|
409
|
+
"secret.rotated",
|
|
410
|
+
"secret.expiring",
|
|
411
|
+
"secret.expired",
|
|
412
|
+
"secret.rotation_due",
|
|
413
|
+
"environment.created",
|
|
414
|
+
"environment.duplicated",
|
|
415
|
+
"environment.promoted",
|
|
416
|
+
"environment.expired",
|
|
417
|
+
"token.created",
|
|
418
|
+
"token.revoked",
|
|
419
|
+
"member.invited",
|
|
420
|
+
"member.joined",
|
|
421
|
+
"member.removed",
|
|
422
|
+
"member.role_changed"
|
|
423
|
+
];
|
|
424
|
+
var CreateWebhookRequestSchema = z.object({
|
|
425
|
+
url: z.string().url(),
|
|
426
|
+
events: z.array(z.enum(WEBHOOK_EVENTS)).min(1),
|
|
427
|
+
kind: z.enum(["generic", "slack"]).default("generic")
|
|
428
|
+
});
|
|
429
|
+
var WebhookSchema = z.object({
|
|
430
|
+
id: z.string(),
|
|
431
|
+
workspaceId: z.string(),
|
|
432
|
+
kind: z.enum(["generic", "slack"]),
|
|
433
|
+
url: z.string(),
|
|
434
|
+
events: z.array(z.string()),
|
|
435
|
+
disabledAt: z.string().nullable(),
|
|
436
|
+
createdAt: z.string()
|
|
437
|
+
});
|
|
438
|
+
var WebhookCreatedSchema = WebhookSchema.extend({
|
|
439
|
+
/** the HMAC signing secret — returned once, at creation, for GENERIC webhooks only */
|
|
440
|
+
secret: z.string().nullable()
|
|
441
|
+
});
|
|
442
|
+
var WebhookDeliverySchema = z.object({
|
|
443
|
+
id: z.string(),
|
|
444
|
+
event: z.string(),
|
|
445
|
+
statusCode: z.number().nullable(),
|
|
446
|
+
success: z.boolean(),
|
|
447
|
+
error: z.string().nullable(),
|
|
448
|
+
durationMs: z.number().nullable(),
|
|
449
|
+
createdAt: z.string()
|
|
450
|
+
});
|
|
451
|
+
var RollbackSecretRequestSchema = z.object({
|
|
452
|
+
version: z.number().int().positive()
|
|
453
|
+
});
|
|
454
|
+
var CreateServiceTokenRequestSchema = z.object({
|
|
455
|
+
name: z.string().min(1),
|
|
456
|
+
projectSlug: z.string().optional(),
|
|
457
|
+
environmentSlug: z.string().optional(),
|
|
458
|
+
access: TokenAccessSchema.default("read"),
|
|
459
|
+
expiresInDays: z.number().int().positive().optional()
|
|
460
|
+
});
|
|
461
|
+
var ImportSecretsRequestSchema = z.object({
|
|
462
|
+
secrets: z.record(z.string(), z.string()),
|
|
463
|
+
prefix: z.string().optional(),
|
|
464
|
+
overwrite: z.boolean().default(false)
|
|
465
|
+
});
|
|
466
|
+
var CreateGitIntegrationRequestSchema = z.object({
|
|
467
|
+
owner: z.string().min(1),
|
|
468
|
+
repo: z.string().min(1),
|
|
469
|
+
/** glob-style, single `*` wildcard, e.g. "preview/*" (default) */
|
|
470
|
+
branchPattern: z.string().min(1).default("preview/*"),
|
|
471
|
+
/** slug of an existing environment new preview environments inherit unset keys from */
|
|
472
|
+
previewEnvironmentTemplate: z.string().optional(),
|
|
473
|
+
defaultTtlDays: z.number().int().positive().default(7)
|
|
474
|
+
});
|
|
475
|
+
var GitIntegrationSchema = z.object({
|
|
476
|
+
id: z.string(),
|
|
477
|
+
projectId: z.string(),
|
|
478
|
+
provider: z.literal("github"),
|
|
479
|
+
owner: z.string(),
|
|
480
|
+
repo: z.string(),
|
|
481
|
+
branchPattern: z.string(),
|
|
482
|
+
previewEnvironmentTemplate: z.string().nullable(),
|
|
483
|
+
defaultTtlDays: z.number(),
|
|
484
|
+
/** the webhook URL to configure on GitHub (Settings -> Webhooks -> Add webhook) */
|
|
485
|
+
webhookUrl: z.string(),
|
|
486
|
+
createdAt: z.string()
|
|
487
|
+
});
|
|
488
|
+
var GitIntegrationCreatedSchema = GitIntegrationSchema.extend({
|
|
489
|
+
/** the HMAC signing secret (GitHub's "Secret" field) — returned once, at creation only */
|
|
490
|
+
webhookSecret: z.string()
|
|
491
|
+
});
|
|
492
|
+
var EnvironmentMatrixSchema = z.object({
|
|
493
|
+
environments: z.array(EnvironmentSchema),
|
|
494
|
+
rows: z.array(z.object({
|
|
495
|
+
key: z.string(),
|
|
496
|
+
cells: z.record(
|
|
497
|
+
z.string(),
|
|
498
|
+
// environmentId
|
|
499
|
+
z.object({
|
|
500
|
+
maskedValue: z.string(),
|
|
501
|
+
secretId: z.string(),
|
|
502
|
+
version: z.number(),
|
|
503
|
+
isReference: z.boolean().optional()
|
|
504
|
+
}).nullable()
|
|
505
|
+
)
|
|
506
|
+
}))
|
|
507
|
+
});
|
|
508
|
+
var EnvironmentDiffSchema = z.object({
|
|
509
|
+
added: z.array(z.string()),
|
|
510
|
+
removed: z.array(z.string()),
|
|
511
|
+
changed: z.array(z.string()),
|
|
512
|
+
unchanged: z.array(z.string())
|
|
513
|
+
});
|
|
514
|
+
var ProjectConfigSchema = z.object({
|
|
515
|
+
version: z.literal(1),
|
|
516
|
+
workspace: z.string(),
|
|
517
|
+
project: z.string(),
|
|
518
|
+
default_environment: z.string().default("development"),
|
|
519
|
+
branch_mapping: z.record(z.string(), z.string()).optional(),
|
|
520
|
+
export: z.object({
|
|
521
|
+
format: z.enum(["dotenv", "json", "yaml", "shell"]).default("dotenv"),
|
|
522
|
+
path: z.string().default(".env"),
|
|
523
|
+
include: z.array(z.string()).default(["*"]),
|
|
524
|
+
exclude: z.array(z.string()).default([])
|
|
525
|
+
}).partial().optional(),
|
|
526
|
+
hooks: z.object({
|
|
527
|
+
post_sync: z.string().optional()
|
|
528
|
+
}).optional()
|
|
529
|
+
});
|
|
530
|
+
var SecretsHealthLocationSchema = z.object({
|
|
531
|
+
projectSlug: SlugSchema,
|
|
532
|
+
projectName: z.string(),
|
|
533
|
+
environmentSlug: SlugSchema,
|
|
534
|
+
environmentName: z.string()
|
|
535
|
+
});
|
|
536
|
+
var StaleSecretSchema = SecretsHealthLocationSchema.extend({
|
|
537
|
+
key: z.string(),
|
|
538
|
+
/** rotatedAt if ever rotated, otherwise the most recent version's createdAt — same
|
|
539
|
+
* definition as `SecretSummary.lastRotatedAt`. */
|
|
540
|
+
lastChangedAt: z.string()
|
|
541
|
+
});
|
|
542
|
+
var ExpiredReminderSchema = SecretsHealthLocationSchema.extend({
|
|
543
|
+
key: z.string(),
|
|
544
|
+
state: z.enum(["expired", "rotation_due"]),
|
|
545
|
+
expiresAt: z.string().nullable(),
|
|
546
|
+
rotationReminderIntervalDays: z.number().int().nullable(),
|
|
547
|
+
lastChangedAt: z.string()
|
|
548
|
+
});
|
|
549
|
+
var InactiveConfigSchema = SecretsHealthLocationSchema.extend({
|
|
550
|
+
environmentId: z.string(),
|
|
551
|
+
/** null means no audit log activity has ever been recorded for this environment. */
|
|
552
|
+
lastActivityAt: z.string().nullable()
|
|
553
|
+
});
|
|
554
|
+
var SecretsHealthSchema = z.object({
|
|
555
|
+
staleDays: z.number().int(),
|
|
556
|
+
inactiveDays: z.number().int(),
|
|
557
|
+
stale: z.array(StaleSecretSchema),
|
|
558
|
+
expiredReminders: z.array(ExpiredReminderSchema),
|
|
559
|
+
inactiveConfigs: z.array(InactiveConfigSchema)
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// src/lib/project-config.ts
|
|
563
|
+
var CONFIG_FILE = ".vaultic.yaml";
|
|
564
|
+
var LOCAL_CONFIG_FILE = ".vaultic.local.yaml";
|
|
565
|
+
function configPath(cwd = process.cwd()) {
|
|
566
|
+
return path2.join(cwd, CONFIG_FILE);
|
|
567
|
+
}
|
|
568
|
+
function loadProjectConfig(cwd = process.cwd()) {
|
|
569
|
+
const p = configPath(cwd);
|
|
570
|
+
if (!existsSync2(p)) return null;
|
|
571
|
+
const raw = yaml.load(readFileSync2(p, "utf8"));
|
|
572
|
+
return ProjectConfigSchema.parse(raw);
|
|
573
|
+
}
|
|
574
|
+
function requireProjectConfig(cwd = process.cwd()) {
|
|
575
|
+
const cfg = loadProjectConfig(cwd);
|
|
576
|
+
if (!cfg) {
|
|
577
|
+
console.error(`No ${CONFIG_FILE} found in this directory. Run \`vaultic init\` first.`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
return cfg;
|
|
581
|
+
}
|
|
582
|
+
function writeProjectConfig(config, cwd = process.cwd()) {
|
|
583
|
+
const header = "# .vaultic.yaml \u2014 safe to commit, contains no secrets\n";
|
|
584
|
+
writeFileSync2(configPath(cwd), header + yaml.dump(config, { noRefs: true }));
|
|
585
|
+
}
|
|
586
|
+
function loadLocalOverrides(cwd = process.cwd()) {
|
|
587
|
+
const p = path2.join(cwd, LOCAL_CONFIG_FILE);
|
|
588
|
+
if (!existsSync2(p)) return {};
|
|
589
|
+
return yaml.load(readFileSync2(p, "utf8")) ?? {};
|
|
590
|
+
}
|
|
591
|
+
function resolveEnvironment(cliFlag, cwd = process.cwd()) {
|
|
592
|
+
if (cliFlag) return cliFlag;
|
|
593
|
+
const local = loadLocalOverrides(cwd);
|
|
594
|
+
if (local.default_environment) return local.default_environment;
|
|
595
|
+
const cfg = requireProjectConfig(cwd);
|
|
596
|
+
return cfg.default_environment;
|
|
597
|
+
}
|
|
598
|
+
function ensureGitignoreEntries(entries, cwd = process.cwd()) {
|
|
599
|
+
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
600
|
+
let content = existsSync2(gitignorePath) ? readFileSync2(gitignorePath, "utf8") : "";
|
|
601
|
+
const existingLines = new Set(content.split("\n").map((l) => l.trim()));
|
|
602
|
+
const toAdd = entries.filter((e) => !existingLines.has(e));
|
|
603
|
+
if (toAdd.length === 0) return;
|
|
604
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
605
|
+
content += toAdd.join("\n") + "\n";
|
|
606
|
+
writeFileSync2(gitignorePath, content);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/commands/init.ts
|
|
610
|
+
import { existsSync as existsSync3 } from "fs";
|
|
611
|
+
async function initCommand() {
|
|
612
|
+
requireCredentials();
|
|
613
|
+
const api = new ApiClient();
|
|
614
|
+
if (existsSync3(configPath())) {
|
|
615
|
+
const { overwrite } = await prompts({
|
|
616
|
+
type: "confirm",
|
|
617
|
+
name: "overwrite",
|
|
618
|
+
message: `${configPath()} already exists. Overwrite?`,
|
|
619
|
+
initial: false
|
|
620
|
+
});
|
|
621
|
+
if (!overwrite) {
|
|
622
|
+
console.log("Aborted.");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const workspaces = await api.get("/workspaces");
|
|
627
|
+
let workspaceSlug;
|
|
628
|
+
if (workspaces.length > 0) {
|
|
629
|
+
const { choice } = await prompts({
|
|
630
|
+
type: "select",
|
|
631
|
+
name: "choice",
|
|
632
|
+
message: "Pick a workspace",
|
|
633
|
+
choices: [
|
|
634
|
+
...workspaces.map((w) => ({ title: `${w.name} (${w.slug})`, value: w.slug })),
|
|
635
|
+
{ title: "+ Create new workspace", value: "__new__" }
|
|
636
|
+
]
|
|
637
|
+
});
|
|
638
|
+
if (choice === void 0) return exitCancelled();
|
|
639
|
+
if (choice === "__new__") {
|
|
640
|
+
workspaceSlug = await createWorkspace(api);
|
|
641
|
+
} else {
|
|
642
|
+
workspaceSlug = choice;
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
console.log("No workspaces yet \u2014 let's create your first one.");
|
|
646
|
+
workspaceSlug = await createWorkspace(api);
|
|
647
|
+
}
|
|
648
|
+
const projects = await api.get(`/workspaces/${workspaceSlug}/projects`);
|
|
649
|
+
let projectSlug;
|
|
650
|
+
if (projects.length > 0) {
|
|
651
|
+
const { choice } = await prompts({
|
|
652
|
+
type: "select",
|
|
653
|
+
name: "choice",
|
|
654
|
+
message: "Pick a project",
|
|
655
|
+
choices: [
|
|
656
|
+
...projects.map((p) => ({ title: `${p.name} (${p.slug})`, value: p.slug })),
|
|
657
|
+
{ title: "+ Create new project", value: "__new__" }
|
|
658
|
+
]
|
|
659
|
+
});
|
|
660
|
+
if (choice === void 0) return exitCancelled();
|
|
661
|
+
if (choice === "__new__") {
|
|
662
|
+
projectSlug = await createProject(api, workspaceSlug);
|
|
663
|
+
} else {
|
|
664
|
+
projectSlug = choice;
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
console.log("No projects yet in this workspace \u2014 let's create one.");
|
|
668
|
+
projectSlug = await createProject(api, workspaceSlug);
|
|
669
|
+
}
|
|
670
|
+
const environments = await api.get(
|
|
671
|
+
`/workspaces/${workspaceSlug}/projects/${projectSlug}/environments`
|
|
672
|
+
);
|
|
673
|
+
const { defaultEnv } = await prompts({
|
|
674
|
+
type: "select",
|
|
675
|
+
name: "defaultEnv",
|
|
676
|
+
message: "Pick your default environment",
|
|
677
|
+
choices: environments.map((e) => ({ title: e.slug, value: e.slug }))
|
|
678
|
+
});
|
|
679
|
+
if (defaultEnv === void 0) return exitCancelled();
|
|
680
|
+
writeProjectConfig({
|
|
681
|
+
version: 1,
|
|
682
|
+
workspace: workspaceSlug,
|
|
683
|
+
project: projectSlug,
|
|
684
|
+
default_environment: defaultEnv,
|
|
685
|
+
export: { format: "dotenv", path: ".env", include: ["*"], exclude: [] }
|
|
686
|
+
});
|
|
687
|
+
console.log(`Wrote ${configPath()}`);
|
|
688
|
+
const { addGitignore } = await prompts({
|
|
689
|
+
type: "confirm",
|
|
690
|
+
name: "addGitignore",
|
|
691
|
+
message: "Add .env, .env.*, and .vaultic.local.yaml to .gitignore?",
|
|
692
|
+
initial: true
|
|
693
|
+
});
|
|
694
|
+
if (addGitignore) {
|
|
695
|
+
ensureGitignoreEntries([".env", ".env.*", ".vaultic.local.yaml"]);
|
|
696
|
+
console.log("Updated .gitignore");
|
|
697
|
+
}
|
|
698
|
+
console.log("\nAll set! Try:");
|
|
699
|
+
console.log(" vaultic secrets set FOO bar");
|
|
700
|
+
console.log(" vaultic run -- printenv FOO");
|
|
701
|
+
}
|
|
702
|
+
async function createWorkspace(api) {
|
|
703
|
+
const { name, slug } = await prompts([
|
|
704
|
+
{ type: "text", name: "name", message: "Workspace name" },
|
|
705
|
+
{ type: "text", name: "slug", message: "Workspace slug", initial: (prev) => slugify(prev) }
|
|
706
|
+
]);
|
|
707
|
+
await api.post("/workspaces", { slug, name });
|
|
708
|
+
return slug;
|
|
709
|
+
}
|
|
710
|
+
async function createProject(api, workspaceSlug) {
|
|
711
|
+
const { name, slug } = await prompts([
|
|
712
|
+
{ type: "text", name: "name", message: "Project name" },
|
|
713
|
+
{ type: "text", name: "slug", message: "Project slug", initial: (prev) => slugify(prev) }
|
|
714
|
+
]);
|
|
715
|
+
await api.post(`/workspaces/${workspaceSlug}/projects`, { slug, name });
|
|
716
|
+
return slug;
|
|
717
|
+
}
|
|
718
|
+
function slugify(value) {
|
|
719
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
720
|
+
}
|
|
721
|
+
function exitCancelled() {
|
|
722
|
+
console.log("Cancelled.");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/commands/auth.ts
|
|
726
|
+
import prompts2 from "prompts";
|
|
727
|
+
async function loginCommand(opts) {
|
|
728
|
+
const serverUrl = opts.server ?? process.env.VAULTIC_API_URL ?? DEFAULT_SERVER_URL;
|
|
729
|
+
if (opts.token) {
|
|
730
|
+
saveCredentials({ serverUrl, token: opts.token });
|
|
731
|
+
console.log(`Saved credentials for ${serverUrl}`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const email = opts.email ?? (await prompts2({ type: "text", name: "email", message: "Email" })).email;
|
|
735
|
+
const password = opts.password ?? (await prompts2({ type: "password", name: "password", message: "Password" })).password;
|
|
736
|
+
if (!email || !password) {
|
|
737
|
+
console.error("Email and password are required.");
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
const api = new ApiClient({ serverUrl, token: "" });
|
|
741
|
+
try {
|
|
742
|
+
const res = await api.post("/auth/login", { email, password });
|
|
743
|
+
saveCredentials({ serverUrl, token: res.token, email: res.user.email });
|
|
744
|
+
console.log(`Logged in as ${res.user.email}`);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
747
|
+
console.log("No account found or wrong password. Register now?");
|
|
748
|
+
const { register } = await prompts2({ type: "confirm", name: "register", message: "Create a new account?", initial: true });
|
|
749
|
+
if (!register) process.exit(1);
|
|
750
|
+
const { name } = await prompts2({ type: "text", name: "name", message: "Your name" });
|
|
751
|
+
const res = await api.post("/auth/register", { email, password, name });
|
|
752
|
+
saveCredentials({ serverUrl, token: res.token, email: res.user.email });
|
|
753
|
+
console.log(`Account created. Logged in as ${res.user.email}`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async function logoutCommand() {
|
|
760
|
+
clearCredentials();
|
|
761
|
+
console.log("Logged out.");
|
|
762
|
+
}
|
|
763
|
+
async function whoamiCommand() {
|
|
764
|
+
const creds = loadCredentials();
|
|
765
|
+
if (!creds) {
|
|
766
|
+
console.log("Not logged in.");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (creds.token.startsWith("vlt_")) {
|
|
770
|
+
console.log(`Authenticated with a service token against ${creds.serverUrl}`);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const api = new ApiClient();
|
|
774
|
+
const me = await api.get("/auth/me");
|
|
775
|
+
console.log(`${me.name} <${me.email}> (${creds.serverUrl})`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/commands/run.ts
|
|
779
|
+
import { spawn } from "child_process";
|
|
780
|
+
|
|
781
|
+
// src/lib/context.ts
|
|
782
|
+
function envBase(cfg, env2) {
|
|
783
|
+
return `/workspaces/${cfg.workspace}/projects/${cfg.project}/environments/${env2}`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/commands/run.ts
|
|
787
|
+
function secretsEqual(a, b) {
|
|
788
|
+
const aKeys = Object.keys(a).sort();
|
|
789
|
+
const bKeys = Object.keys(b).sort();
|
|
790
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
791
|
+
return aKeys.every((k, i) => k === bKeys[i] && a[k] === b[k]);
|
|
792
|
+
}
|
|
793
|
+
async function runCommand(commandParts, opts) {
|
|
794
|
+
if (commandParts.length === 0) {
|
|
795
|
+
console.error("Usage: vaultic run [-e <environment>] [--watch] -- <command> [args...]");
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
const cfg = requireProjectConfig();
|
|
799
|
+
const env2 = resolveEnvironment(opts.env);
|
|
800
|
+
const api = new ApiClient();
|
|
801
|
+
const [cmd, ...args] = commandParts;
|
|
802
|
+
const fetchSecrets = () => api.get(`${envBase(cfg, env2)}/export`);
|
|
803
|
+
if (!opts.watch) {
|
|
804
|
+
const secrets2 = await fetchSecrets();
|
|
805
|
+
const child2 = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...secrets2 } });
|
|
806
|
+
child2.on("exit", (code, signal) => {
|
|
807
|
+
if (signal) process.kill(process.pid, signal);
|
|
808
|
+
else process.exit(code ?? 0);
|
|
809
|
+
});
|
|
810
|
+
child2.on("error", (err) => {
|
|
811
|
+
console.error(`Failed to run "${cmd}": ${err.message}`);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const pollIntervalMs = Math.max(500, Number(opts.pollInterval ?? "2000"));
|
|
817
|
+
let current = await fetchSecrets();
|
|
818
|
+
let child = null;
|
|
819
|
+
let stopped = false;
|
|
820
|
+
let restarting = false;
|
|
821
|
+
function spawnChild() {
|
|
822
|
+
child = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...current } });
|
|
823
|
+
child.on("exit", (code, signal) => {
|
|
824
|
+
if (stopped || restarting) return;
|
|
825
|
+
stopped = true;
|
|
826
|
+
clearInterval(poller);
|
|
827
|
+
if (signal) process.kill(process.pid, signal);
|
|
828
|
+
else process.exit(code ?? 0);
|
|
829
|
+
});
|
|
830
|
+
child.on("error", (err) => {
|
|
831
|
+
console.error(`Failed to run "${cmd}": ${err.message}`);
|
|
832
|
+
process.exit(1);
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
async function killChild() {
|
|
836
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
837
|
+
await new Promise((resolve) => {
|
|
838
|
+
child.once("exit", () => resolve());
|
|
839
|
+
child.kill("SIGTERM");
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
console.log(`vaultic: watching "${env2}" for secret changes (polling every ${pollIntervalMs}ms)`);
|
|
843
|
+
spawnChild();
|
|
844
|
+
const poller = setInterval(async () => {
|
|
845
|
+
if (stopped || restarting) return;
|
|
846
|
+
let next;
|
|
847
|
+
try {
|
|
848
|
+
next = await fetchSecrets();
|
|
849
|
+
} catch (err) {
|
|
850
|
+
console.error(`vaultic: watch poll failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (secretsEqual(current, next)) return;
|
|
854
|
+
restarting = true;
|
|
855
|
+
console.log(`vaultic: secrets changed in "${env2}" -> restarting "${cmd}"`);
|
|
856
|
+
current = next;
|
|
857
|
+
await killChild();
|
|
858
|
+
restarting = false;
|
|
859
|
+
if (!stopped) spawnChild();
|
|
860
|
+
}, pollIntervalMs);
|
|
861
|
+
const shutdown = async (signal) => {
|
|
862
|
+
if (stopped) return;
|
|
863
|
+
stopped = true;
|
|
864
|
+
clearInterval(poller);
|
|
865
|
+
await killChild();
|
|
866
|
+
process.kill(process.pid, signal);
|
|
867
|
+
};
|
|
868
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
869
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/commands/export.ts
|
|
873
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
874
|
+
import path3 from "path";
|
|
875
|
+
|
|
876
|
+
// src/lib/formats.ts
|
|
877
|
+
import { createHash } from "crypto";
|
|
878
|
+
import yaml2 from "js-yaml";
|
|
879
|
+
var GENERATED_HEADER = "# Generated by Vaultic \u2014 do not commit";
|
|
880
|
+
function dotenvEscape(value) {
|
|
881
|
+
if (/^[A-Za-z0-9_.:/@-]*$/.test(value)) return value;
|
|
882
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
883
|
+
}
|
|
884
|
+
function checksum(values) {
|
|
885
|
+
const sorted = Object.keys(values).sort().map((k) => `${k}=${values[k]}`).join("\n");
|
|
886
|
+
return createHash("sha256").update(sorted).digest("hex");
|
|
887
|
+
}
|
|
888
|
+
function renderBody(values, format) {
|
|
889
|
+
const keys = Object.keys(values).sort();
|
|
890
|
+
switch (format) {
|
|
891
|
+
case "dotenv":
|
|
892
|
+
return keys.map((k) => `${k}=${dotenvEscape(values[k])}`).join("\n") + "\n";
|
|
893
|
+
case "shell":
|
|
894
|
+
return keys.map((k) => `export ${k}=${dotenvEscape(values[k])}`).join("\n") + "\n";
|
|
895
|
+
case "json":
|
|
896
|
+
return JSON.stringify(values, null, 2) + "\n";
|
|
897
|
+
case "yaml":
|
|
898
|
+
return yaml2.dump(values, { noRefs: true });
|
|
899
|
+
default:
|
|
900
|
+
throw new Error(`Unknown format: ${format}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function renderFile(values, format, includeHeader = true) {
|
|
904
|
+
const body = renderBody(values, format);
|
|
905
|
+
if (!includeHeader || format === "json") return body;
|
|
906
|
+
const header = `${GENERATED_HEADER}
|
|
907
|
+
# checksum: ${checksum(values)}
|
|
908
|
+
`;
|
|
909
|
+
return header + body;
|
|
910
|
+
}
|
|
911
|
+
function extractChecksum(fileContents) {
|
|
912
|
+
const match = fileContents.match(/^#\s*checksum:\s*([a-f0-9]{64})\s*$/m);
|
|
913
|
+
return match ? match[1] : null;
|
|
914
|
+
}
|
|
915
|
+
function parseDotenv(contents) {
|
|
916
|
+
const values = {};
|
|
917
|
+
for (const rawLine of contents.split("\n")) {
|
|
918
|
+
const line = rawLine.trim();
|
|
919
|
+
if (!line || line.startsWith("#")) continue;
|
|
920
|
+
const eq = line.indexOf("=");
|
|
921
|
+
if (eq === -1) continue;
|
|
922
|
+
const key = line.slice(0, eq).trim().replace(/^export\s+/, "");
|
|
923
|
+
let value = line.slice(eq + 1).trim();
|
|
924
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
925
|
+
value = value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
926
|
+
}
|
|
927
|
+
values[key] = value;
|
|
928
|
+
}
|
|
929
|
+
return values;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/lib/glob.ts
|
|
933
|
+
function globToRegExp(glob) {
|
|
934
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
935
|
+
return new RegExp(`^${escaped}$`);
|
|
936
|
+
}
|
|
937
|
+
function filterKeys(keys, include, exclude) {
|
|
938
|
+
const includeRe = include.length > 0 ? include.map(globToRegExp) : [/.*/];
|
|
939
|
+
const excludeRe = exclude.map(globToRegExp);
|
|
940
|
+
return keys.filter((k) => includeRe.some((re) => re.test(k)) && !excludeRe.some((re) => re.test(k)));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/commands/export.ts
|
|
944
|
+
async function exportCommand(opts) {
|
|
945
|
+
const cfg = requireProjectConfig();
|
|
946
|
+
const env2 = resolveEnvironment(opts.env);
|
|
947
|
+
const api = new ApiClient();
|
|
948
|
+
const all = await api.get(`${envBase(cfg, env2)}/export`);
|
|
949
|
+
const exportCfg = cfg.export ?? {};
|
|
950
|
+
const format = opts.format ?? exportCfg.format ?? "dotenv";
|
|
951
|
+
const outPath = opts.path ?? exportCfg.path ?? ".env";
|
|
952
|
+
const include = exportCfg.include ?? ["*"];
|
|
953
|
+
const exclude = exportCfg.exclude ?? [];
|
|
954
|
+
const keys = filterKeys(Object.keys(all), include, exclude);
|
|
955
|
+
const values = Object.fromEntries(keys.map((k) => [k, all[k]]));
|
|
956
|
+
if (opts.check) {
|
|
957
|
+
if (!existsSync4(outPath)) {
|
|
958
|
+
console.error(`${outPath} does not exist locally. Run \`vaultic export\` or \`vaultic sync\`.`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
const local = readFileSync3(outPath, "utf8");
|
|
962
|
+
const localChecksum = extractChecksum(local);
|
|
963
|
+
const serverChecksum = checksum(values);
|
|
964
|
+
if (localChecksum !== serverChecksum) {
|
|
965
|
+
console.error(`${outPath} is stale vs server for environment "${env2}". Run \`vaultic sync\`.`);
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
console.log(`${outPath} is up to date.`);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const content = renderFile(values, format, !opts.noHeader);
|
|
972
|
+
if (opts.format && !opts.path) {
|
|
973
|
+
process.stdout.write(content);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
writeFileSync3(path3.resolve(outPath), content);
|
|
977
|
+
console.error(`Wrote ${keys.length} secrets to ${outPath} (${format}) for environment "${env2}"`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/commands/import.ts
|
|
981
|
+
import { readFileSync as readFileSync4, unlinkSync, existsSync as existsSync5 } from "fs";
|
|
982
|
+
import prompts3 from "prompts";
|
|
983
|
+
async function importCommand(file, opts) {
|
|
984
|
+
const cfg = requireProjectConfig();
|
|
985
|
+
const env2 = resolveEnvironment(opts.env);
|
|
986
|
+
const raw = readFileSync4(file, "utf8");
|
|
987
|
+
const useJson = opts.fromJson || file.endsWith(".json");
|
|
988
|
+
const secrets2 = useJson ? JSON.parse(raw) : parseDotenv(raw);
|
|
989
|
+
if (Object.keys(secrets2).length === 0) {
|
|
990
|
+
console.log("No key/value pairs found to import.");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (opts.dryRun) {
|
|
994
|
+
console.log(`Would import ${Object.keys(secrets2).length} keys into "${env2}":`);
|
|
995
|
+
for (const key of Object.keys(secrets2)) {
|
|
996
|
+
const finalKey = (opts.prefix ? `${opts.prefix}${key}` : key).toUpperCase();
|
|
997
|
+
console.log(` ${finalKey}`);
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const api = new ApiClient();
|
|
1002
|
+
const result = await api.post(
|
|
1003
|
+
`${envBase(cfg, env2)}/secrets/import`,
|
|
1004
|
+
{ secrets: secrets2, prefix: opts.prefix, overwrite: opts.overwrite ?? false }
|
|
1005
|
+
);
|
|
1006
|
+
console.log(`Created: ${result.created.length}, Updated: ${result.updated.length}, Skipped: ${result.skipped.length}`);
|
|
1007
|
+
if (result.skipped.length > 0) {
|
|
1008
|
+
console.log(`Skipped (already exist, rerun with --overwrite to replace): ${result.skipped.join(", ")}`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!useJson && existsSync5(file) && process.stdout.isTTY) {
|
|
1011
|
+
const { del } = await prompts3({
|
|
1012
|
+
type: "confirm",
|
|
1013
|
+
name: "del",
|
|
1014
|
+
message: `Delete local ${file} and switch to \`vaultic run\`?`,
|
|
1015
|
+
initial: false
|
|
1016
|
+
});
|
|
1017
|
+
if (del) {
|
|
1018
|
+
unlinkSync(file);
|
|
1019
|
+
console.log(`Deleted ${file}. Use \`vaultic run -- <cmd>\` from now on.`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/commands/status.ts
|
|
1025
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1026
|
+
function lifecycleWarnings(secrets2) {
|
|
1027
|
+
const lines = [];
|
|
1028
|
+
for (const s of secrets2) {
|
|
1029
|
+
const expiry = secretExpiryState(s.expiresAt);
|
|
1030
|
+
if (expiry === "expired") lines.push(` [expired] ${s.key} \u2014 expired ${s.expiresAt}`);
|
|
1031
|
+
else if (expiry === "expiring") lines.push(` [expiring soon] ${s.key} \u2014 expires ${s.expiresAt}`);
|
|
1032
|
+
if (isRotationDue(s.rotationReminderIntervalDays, s.lastRotatedAt)) {
|
|
1033
|
+
lines.push(` [rotation due] ${s.key} \u2014 overdue for rotation (every ${s.rotationReminderIntervalDays}d)`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return lines;
|
|
1037
|
+
}
|
|
1038
|
+
async function statusCommand(opts) {
|
|
1039
|
+
const cfg = requireProjectConfig();
|
|
1040
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1041
|
+
const outPath = cfg.export?.path ?? ".env";
|
|
1042
|
+
console.log(`On environment "${env2}" (${cfg.workspace}/${cfg.project})`);
|
|
1043
|
+
const api = new ApiClient();
|
|
1044
|
+
const server = await api.get(`${envBase(cfg, env2)}/export`);
|
|
1045
|
+
const secrets2 = await api.get(`${envBase(cfg, env2)}/secrets`);
|
|
1046
|
+
const warnings = lifecycleWarnings(secrets2);
|
|
1047
|
+
function printWarnings() {
|
|
1048
|
+
if (warnings.length === 0) return;
|
|
1049
|
+
console.log("");
|
|
1050
|
+
console.log("Attention needed:");
|
|
1051
|
+
for (const line of warnings) console.log(line);
|
|
1052
|
+
}
|
|
1053
|
+
if (!existsSync6(outPath)) {
|
|
1054
|
+
console.log(`No local ${outPath} \u2014 run \`vaultic sync\` or \`vaultic export\` to generate one.`);
|
|
1055
|
+
console.log(`Server has ${Object.keys(server).length} keys.`);
|
|
1056
|
+
printWarnings();
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const local = parseDotenv(readFileSync5(outPath, "utf8"));
|
|
1060
|
+
const localKeys = new Set(Object.keys(local));
|
|
1061
|
+
const serverKeys = new Set(Object.keys(server));
|
|
1062
|
+
const added = [];
|
|
1063
|
+
const removed = [];
|
|
1064
|
+
const changed = [];
|
|
1065
|
+
for (const k of serverKeys) if (!localKeys.has(k)) added.push(k);
|
|
1066
|
+
for (const k of localKeys) if (!serverKeys.has(k)) removed.push(k);
|
|
1067
|
+
for (const k of serverKeys) if (localKeys.has(k) && local[k] !== server[k]) changed.push(k);
|
|
1068
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
1069
|
+
console.log(`${outPath} is up to date with the server.`);
|
|
1070
|
+
printWarnings();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
console.log(`${outPath} differs from the server:`);
|
|
1074
|
+
if (added.length) console.log(` new on server: ${added.join(", ")}`);
|
|
1075
|
+
if (removed.length) console.log(` only local: ${removed.join(", ")}`);
|
|
1076
|
+
if (changed.length) console.log(` changed: ${changed.join(", ")}`);
|
|
1077
|
+
console.log("Run `vaultic sync` to pull the latest values.");
|
|
1078
|
+
printWarnings();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/commands/sync.ts
|
|
1082
|
+
import { execSync } from "child_process";
|
|
1083
|
+
async function syncCommand(opts) {
|
|
1084
|
+
const cfg = requireProjectConfig();
|
|
1085
|
+
await exportCommand({ env: opts.env });
|
|
1086
|
+
const hook = cfg.hooks?.post_sync;
|
|
1087
|
+
if (hook) {
|
|
1088
|
+
console.log(`Running post_sync hook: ${hook}`);
|
|
1089
|
+
execSync(hook, { stdio: "inherit" });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/commands/secrets.ts
|
|
1094
|
+
import readline from "readline";
|
|
1095
|
+
|
|
1096
|
+
// 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
|
+
var postgresRotationPlugin = {
|
|
1107
|
+
name: "postgres",
|
|
1108
|
+
describe() {
|
|
1109
|
+
return "Rotates a PostgreSQL role's password via `ALTER ROLE ... WITH PASSWORD ...`. 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
|
+
},
|
|
1111
|
+
async rotate(ctx) {
|
|
1112
|
+
let targetUrl;
|
|
1113
|
+
try {
|
|
1114
|
+
targetUrl = new URL(ctx.currentValue);
|
|
1115
|
+
} catch {
|
|
1116
|
+
throw new Error(
|
|
1117
|
+
`Current value for "${ctx.key}" isn't a valid URL \u2014 expected a postgres://user:password@host:port/db connection string.`
|
|
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
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
// src/rotation/index.ts
|
|
1179
|
+
var PLUGINS = [postgresRotationPlugin, stripeRotationPlugin, awsIamRotationPlugin];
|
|
1180
|
+
function getRotationPlugin(name) {
|
|
1181
|
+
const plugin = PLUGINS.find((p) => p.name === name);
|
|
1182
|
+
if (!plugin) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Unknown rotation provider "${name}". Available: ${PLUGINS.map((p) => p.name).join(", ")}.`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return plugin;
|
|
1188
|
+
}
|
|
1189
|
+
function listRotationPlugins() {
|
|
1190
|
+
return PLUGINS;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/commands/secrets.ts
|
|
1194
|
+
function readStdin() {
|
|
1195
|
+
return new Promise((resolve) => {
|
|
1196
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
1197
|
+
const lines = [];
|
|
1198
|
+
rl.on("line", (l) => lines.push(l));
|
|
1199
|
+
rl.on("close", () => resolve(lines.join("\n")));
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
function annotate(s) {
|
|
1203
|
+
const tags = [];
|
|
1204
|
+
if (s.inheritedFrom) tags.push(`inherited from ${s.inheritedFrom}`);
|
|
1205
|
+
if (s.hasPersonalOverride) tags.push("personal override active");
|
|
1206
|
+
if (s.isReference) tags.push("reference");
|
|
1207
|
+
const expiry = secretExpiryState(s.expiresAt);
|
|
1208
|
+
if (expiry === "expired") tags.push(`EXPIRED ${s.expiresAt}`);
|
|
1209
|
+
else if (expiry === "expiring") tags.push(`expiring ${s.expiresAt}`);
|
|
1210
|
+
if (isRotationDue(s.rotationReminderIntervalDays, s.lastRotatedAt)) {
|
|
1211
|
+
tags.push(`rotation overdue (every ${s.rotationReminderIntervalDays}d)`);
|
|
1212
|
+
}
|
|
1213
|
+
return tags.length ? ` (${tags.join(", ")})` : "";
|
|
1214
|
+
}
|
|
1215
|
+
function buildExpiryMetadata(opts) {
|
|
1216
|
+
const meta = {};
|
|
1217
|
+
let any = false;
|
|
1218
|
+
if (opts.clearExpiry) {
|
|
1219
|
+
meta.expiresAt = null;
|
|
1220
|
+
any = true;
|
|
1221
|
+
} else if (opts.expiresAt) {
|
|
1222
|
+
const parsed = new Date(opts.expiresAt);
|
|
1223
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1224
|
+
console.error(`Invalid --expires-at date: "${opts.expiresAt}"`);
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
meta.expiresAt = parsed.toISOString();
|
|
1228
|
+
any = true;
|
|
1229
|
+
} else if (opts.expiresIn) {
|
|
1230
|
+
meta.expiresAt = new Date(Date.now() + parseDurationMs(opts.expiresIn)).toISOString();
|
|
1231
|
+
any = true;
|
|
1232
|
+
}
|
|
1233
|
+
if (opts.clearRemind) {
|
|
1234
|
+
meta.rotationReminderIntervalDays = null;
|
|
1235
|
+
any = true;
|
|
1236
|
+
} else if (opts.remindEvery) {
|
|
1237
|
+
meta.rotationReminderIntervalDays = Math.max(1, Math.round(parseDurationMs(opts.remindEvery) / 864e5));
|
|
1238
|
+
any = true;
|
|
1239
|
+
}
|
|
1240
|
+
return any ? meta : void 0;
|
|
1241
|
+
}
|
|
1242
|
+
function describeMetadata(secret) {
|
|
1243
|
+
const parts = [];
|
|
1244
|
+
if (secret.expiresAt) parts.push(`expires ${secret.expiresAt}`);
|
|
1245
|
+
if (secret.rotationReminderIntervalDays) parts.push(`remind every ${secret.rotationReminderIntervalDays}d`);
|
|
1246
|
+
return parts.length ? ` (${parts.join(", ")})` : "";
|
|
1247
|
+
}
|
|
1248
|
+
async function secretsListCommand(opts) {
|
|
1249
|
+
const cfg = requireProjectConfig();
|
|
1250
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1251
|
+
const api = new ApiClient();
|
|
1252
|
+
const secrets2 = await api.get(`${envBase(cfg, env2)}/secrets`);
|
|
1253
|
+
if (opts.reveal) {
|
|
1254
|
+
const revealed = await Promise.all(
|
|
1255
|
+
secrets2.map((s) => api.post(`${envBase(cfg, env2)}/secrets/${s.key}/reveal`))
|
|
1256
|
+
);
|
|
1257
|
+
if (opts.json) {
|
|
1258
|
+
console.log(JSON.stringify(revealed, null, 2));
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
for (const s of revealed) console.log(`${s.key}=${s.value}${annotate(s)}`);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (opts.json) {
|
|
1265
|
+
console.log(JSON.stringify(secrets2, null, 2));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
for (const s of secrets2) console.log(`${s.key}=${s.maskedValue}${annotate(s)}`);
|
|
1269
|
+
}
|
|
1270
|
+
async function secretsGetCommand(key, opts) {
|
|
1271
|
+
const cfg = requireProjectConfig();
|
|
1272
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1273
|
+
const api = new ApiClient();
|
|
1274
|
+
const suffix = opts.previous ? "?previous=true" : "";
|
|
1275
|
+
const secret = opts.reveal ? await api.post(`${envBase(cfg, env2)}/secrets/${key}/reveal${suffix}`) : await api.get(`${envBase(cfg, env2)}/secrets/${key}${suffix}`);
|
|
1276
|
+
if (opts.json) {
|
|
1277
|
+
console.log(JSON.stringify(secret, null, 2));
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
console.log("value" in secret ? secret.value : secret.maskedValue);
|
|
1281
|
+
}
|
|
1282
|
+
async function secretsRotateCommand(key, value, opts) {
|
|
1283
|
+
const cfg = requireProjectConfig();
|
|
1284
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1285
|
+
const api = new ApiClient();
|
|
1286
|
+
let resolvedValue = value;
|
|
1287
|
+
if (opts.provider) {
|
|
1288
|
+
if (value !== void 0) {
|
|
1289
|
+
console.error(`Don't pass both a value and --provider \u2014 the plugin computes the new value itself.`);
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
const plugin = getRotationPlugin(opts.provider);
|
|
1293
|
+
const current = await api.post(`${envBase(cfg, env2)}/secrets/${key}/reveal`);
|
|
1294
|
+
const result = await plugin.rotate({
|
|
1295
|
+
key,
|
|
1296
|
+
currentValue: current.value,
|
|
1297
|
+
options: { role: opts.role, adminConnString: opts.adminConnString, newPassword: opts.newPassword }
|
|
1298
|
+
});
|
|
1299
|
+
console.log(result.summary);
|
|
1300
|
+
resolvedValue = result.newValue;
|
|
1301
|
+
} else if (opts.fromFile) {
|
|
1302
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
1303
|
+
resolvedValue = readFileSync6(opts.fromFile, "utf8").replace(/\n$/, "");
|
|
1304
|
+
} else if (value === "-") {
|
|
1305
|
+
resolvedValue = await readStdin();
|
|
1306
|
+
}
|
|
1307
|
+
if (resolvedValue === void 0) {
|
|
1308
|
+
console.error("Provide a new value, use --from-file <path>, --provider <name>, or pass '-' to read from stdin.");
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
const secret = await api.post(`${envBase(cfg, env2)}/secrets/${key}/rotate`, {
|
|
1312
|
+
value: resolvedValue,
|
|
1313
|
+
graceWindow: opts.grace
|
|
1314
|
+
});
|
|
1315
|
+
console.log(
|
|
1316
|
+
`Rotated ${secret.key} in ${env2} (now v${secret.version}). ` + (opts.grace ? `Previous value fetchable via 'vaultic secrets get ${key} --previous' until ${secret.rotationGraceUntil}.` : `Previous value fetchable indefinitely via 'vaultic secrets get ${key} --previous' (no --grace given).`)
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
function rotationProvidersCommand() {
|
|
1320
|
+
for (const plugin of listRotationPlugins()) {
|
|
1321
|
+
console.log(`${plugin.name}
|
|
1322
|
+
${plugin.describe()}
|
|
1323
|
+
`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function secretsSetCommand(key, value, opts) {
|
|
1327
|
+
const cfg = requireProjectConfig();
|
|
1328
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1329
|
+
const api = new ApiClient();
|
|
1330
|
+
let resolvedValue = value;
|
|
1331
|
+
if (opts.fromFile) {
|
|
1332
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
1333
|
+
resolvedValue = readFileSync6(opts.fromFile, "utf8").replace(/\n$/, "");
|
|
1334
|
+
} else if (value === "-") {
|
|
1335
|
+
resolvedValue = await readStdin();
|
|
1336
|
+
}
|
|
1337
|
+
const metadata = buildExpiryMetadata(opts);
|
|
1338
|
+
if (resolvedValue === void 0) {
|
|
1339
|
+
if (!metadata) {
|
|
1340
|
+
console.error(
|
|
1341
|
+
"Provide a value, use --from-file <path>, pass '-' to read from stdin, or set --expires-in/--expires-at/--remind-every/--clear-expiry/--clear-remind to update metadata only."
|
|
1342
|
+
);
|
|
1343
|
+
process.exit(1);
|
|
1344
|
+
}
|
|
1345
|
+
const secret2 = await api.patch(`${envBase(cfg, env2)}/secrets/${key}/metadata`, metadata);
|
|
1346
|
+
console.log(`Updated ${secret2.key} in ${env2}${describeMetadata(secret2)}`);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const secret = await api.post(
|
|
1350
|
+
`${envBase(cfg, env2)}/secrets`,
|
|
1351
|
+
{ key, value: resolvedValue, ...metadata }
|
|
1352
|
+
);
|
|
1353
|
+
if (secret.status === "pending") {
|
|
1354
|
+
console.log(`"${env2}" is locked \u2014 proposed change for ${key} (proposal ${secret.proposalId}), awaiting approval.`);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
console.log(`Set ${secret.key} in ${env2} (v${secret.version})${describeMetadata(secret)}`);
|
|
1358
|
+
}
|
|
1359
|
+
async function secretsDeleteCommand(key, opts) {
|
|
1360
|
+
const cfg = requireProjectConfig();
|
|
1361
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1362
|
+
const api = new ApiClient();
|
|
1363
|
+
const result = await api.delete(
|
|
1364
|
+
`${envBase(cfg, env2)}/secrets/${key}`
|
|
1365
|
+
);
|
|
1366
|
+
if (result?.status === "pending") {
|
|
1367
|
+
console.log(`"${env2}" is locked \u2014 proposed deletion of ${key} (proposal ${result.proposalId}), awaiting approval.`);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
console.log(`Deleted ${key} from ${env2}`);
|
|
1371
|
+
}
|
|
1372
|
+
async function secretsHistoryCommand(key, opts) {
|
|
1373
|
+
const cfg = requireProjectConfig();
|
|
1374
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1375
|
+
const api = new ApiClient();
|
|
1376
|
+
const versions = await api.get(
|
|
1377
|
+
`${envBase(cfg, env2)}/secrets/${key}/history${opts.reveal ? "?reveal=true" : ""}`
|
|
1378
|
+
);
|
|
1379
|
+
for (const v of versions) {
|
|
1380
|
+
console.log(`v${v.version} ${v.changeType} ${v.createdAt}${v.value !== void 0 ? ` ${v.value}` : ""}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
async function secretsRollbackCommand(key, opts) {
|
|
1384
|
+
const cfg = requireProjectConfig();
|
|
1385
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1386
|
+
const api = new ApiClient();
|
|
1387
|
+
const secret = await api.post(`${envBase(cfg, env2)}/secrets/${key}/rollback`, {
|
|
1388
|
+
version: Number(opts.version)
|
|
1389
|
+
});
|
|
1390
|
+
console.log(`Rolled back ${secret.key} to v${opts.version} (now v${secret.version})`);
|
|
1391
|
+
}
|
|
1392
|
+
async function secretsRenameCommand(oldKey, newKey, opts) {
|
|
1393
|
+
const cfg = requireProjectConfig();
|
|
1394
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1395
|
+
const api = new ApiClient();
|
|
1396
|
+
await api.post(`${envBase(cfg, env2)}/secrets/${oldKey}/rename`, { newKey });
|
|
1397
|
+
console.log(`Renamed ${oldKey} -> ${newKey} in ${env2}`);
|
|
1398
|
+
}
|
|
1399
|
+
async function secretsOverrideSetCommand(key, value, opts) {
|
|
1400
|
+
const cfg = requireProjectConfig();
|
|
1401
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1402
|
+
const api = new ApiClient();
|
|
1403
|
+
let resolvedValue = value;
|
|
1404
|
+
if (opts.fromFile) {
|
|
1405
|
+
const { readFileSync: readFileSync6 } = await import("fs");
|
|
1406
|
+
resolvedValue = readFileSync6(opts.fromFile, "utf8").replace(/\n$/, "");
|
|
1407
|
+
} else if (value === "-") {
|
|
1408
|
+
resolvedValue = await readStdin();
|
|
1409
|
+
}
|
|
1410
|
+
if (resolvedValue === void 0) {
|
|
1411
|
+
console.error("Provide a value, use --from-file <path>, or pass '-' to read from stdin.");
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
await api.put(`${envBase(cfg, env2)}/secrets/${key}/override`, { value: resolvedValue });
|
|
1415
|
+
console.log(`Set personal override for ${key} in ${env2} (visible only to you)`);
|
|
1416
|
+
}
|
|
1417
|
+
async function secretsOverrideClearCommand(key, opts) {
|
|
1418
|
+
const cfg = requireProjectConfig();
|
|
1419
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1420
|
+
const api = new ApiClient();
|
|
1421
|
+
await api.delete(`${envBase(cfg, env2)}/secrets/${key}/override`);
|
|
1422
|
+
console.log(`Cleared personal override for ${key} in ${env2}`);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/commands/env.ts
|
|
1426
|
+
import prompts4 from "prompts";
|
|
1427
|
+
function projectBase(workspace2, project) {
|
|
1428
|
+
return `/workspaces/${workspace2}/projects/${project}`;
|
|
1429
|
+
}
|
|
1430
|
+
async function envListCommand() {
|
|
1431
|
+
const cfg = requireProjectConfig();
|
|
1432
|
+
const api = new ApiClient();
|
|
1433
|
+
const envs = await api.get(`${projectBase(cfg.workspace, cfg.project)}/environments`);
|
|
1434
|
+
for (const e of envs) {
|
|
1435
|
+
const marker = e.slug === cfg.default_environment ? "*" : " ";
|
|
1436
|
+
const tags = [];
|
|
1437
|
+
if (e.parentSlug) tags.push(`inherits ${e.parentSlug}`);
|
|
1438
|
+
if (e.lockedAt) tags.push("locked");
|
|
1439
|
+
if (e.isEphemeral) tags.push(`ephemeral, expires ${e.expiresAt ?? "?"}`);
|
|
1440
|
+
console.log(`${marker} ${e.slug}${tags.length ? ` (${tags.join(", ")})` : ""}`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
async function envCreateCommand(slug, opts = {}) {
|
|
1444
|
+
const cfg = requireProjectConfig();
|
|
1445
|
+
const api = new ApiClient();
|
|
1446
|
+
await api.post(`${projectBase(cfg.workspace, cfg.project)}/environments`, {
|
|
1447
|
+
slug,
|
|
1448
|
+
inherits: opts.inherits,
|
|
1449
|
+
ephemeral: opts.ephemeral,
|
|
1450
|
+
ttlDays: opts.ttlDays ? Number(opts.ttlDays) : void 0,
|
|
1451
|
+
expiresAt: opts.expiresAt
|
|
1452
|
+
});
|
|
1453
|
+
const tags = [];
|
|
1454
|
+
if (opts.inherits) tags.push(`inherits from "${opts.inherits}"`);
|
|
1455
|
+
if (opts.ephemeral) tags.push(`ephemeral, ttl ${opts.ttlDays ?? "?"}d`);
|
|
1456
|
+
console.log(`Created environment "${slug}"${tags.length ? ` (${tags.join(", ")})` : ""}`);
|
|
1457
|
+
}
|
|
1458
|
+
async function envDuplicateCommand(source, target, opts) {
|
|
1459
|
+
const cfg = requireProjectConfig();
|
|
1460
|
+
const api = new ApiClient();
|
|
1461
|
+
const mode = opts.link ? "link" : opts.keysOnly ? "keys-only" : "values";
|
|
1462
|
+
await api.post(`${projectBase(cfg.workspace, cfg.project)}/environments/${source}/duplicate`, {
|
|
1463
|
+
targetSlug: target,
|
|
1464
|
+
mode
|
|
1465
|
+
});
|
|
1466
|
+
console.log(`Duplicated "${source}" -> "${target}" (${mode})`);
|
|
1467
|
+
}
|
|
1468
|
+
async function envDiffCommand(from, to, opts) {
|
|
1469
|
+
const cfg = requireProjectConfig();
|
|
1470
|
+
const api = new ApiClient();
|
|
1471
|
+
const diff = await api.get(
|
|
1472
|
+
`${projectBase(cfg.workspace, cfg.project)}/diff?from=${from}&to=${to}${opts.reveal ? "&reveal=true" : ""}`
|
|
1473
|
+
);
|
|
1474
|
+
console.log(`Diff ${from} -> ${to}`);
|
|
1475
|
+
console.log(` added: ${diff.added.join(", ") || "(none)"}`);
|
|
1476
|
+
console.log(` removed: ${diff.removed.join(", ") || "(none)"}`);
|
|
1477
|
+
console.log(` changed: ${diff.changed.join(", ") || "(none)"}`);
|
|
1478
|
+
console.log(` unchanged: ${diff.unchanged.join(", ") || "(none)"}`);
|
|
1479
|
+
}
|
|
1480
|
+
async function envPromoteCommand(from, to, opts) {
|
|
1481
|
+
const cfg = requireProjectConfig();
|
|
1482
|
+
const api = new ApiClient();
|
|
1483
|
+
const base2 = projectBase(cfg.workspace, cfg.project);
|
|
1484
|
+
const diff = await api.get(`${base2}/diff?from=${from}&to=${to}`);
|
|
1485
|
+
const toCreate = diff.removed;
|
|
1486
|
+
const toUpdate = diff.changed;
|
|
1487
|
+
if (toCreate.length === 0 && toUpdate.length === 0) {
|
|
1488
|
+
console.log(`"${to}" is already up to date with "${from}" \u2014 nothing to promote.`);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
console.log(`Promoting ${from} -> ${to}:`);
|
|
1492
|
+
console.log(` create: ${toCreate.join(", ") || "(none)"}`);
|
|
1493
|
+
console.log(` update: ${toUpdate.join(", ") || "(none)"}`);
|
|
1494
|
+
console.log(`(values masked; ${diff.added.length} key(s) only in "${to}" are left untouched)`);
|
|
1495
|
+
if (!opts.yes) {
|
|
1496
|
+
const { proceed } = await prompts4({
|
|
1497
|
+
type: "confirm",
|
|
1498
|
+
name: "proceed",
|
|
1499
|
+
message: `Apply these ${toCreate.length + toUpdate.length} change(s) to "${to}"?`,
|
|
1500
|
+
initial: false
|
|
1501
|
+
});
|
|
1502
|
+
if (!proceed) {
|
|
1503
|
+
console.log("Aborted.");
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const result = await api.post(`${base2}/environments/${from}/promote`, { to });
|
|
1508
|
+
console.log(`Promoted ${result.promoted.length} key(s) to "${to}".`);
|
|
1509
|
+
}
|
|
1510
|
+
async function envLockCommand(slug) {
|
|
1511
|
+
const cfg = requireProjectConfig();
|
|
1512
|
+
const api = new ApiClient();
|
|
1513
|
+
await api.post(`${projectBase(cfg.workspace, cfg.project)}/environments/${slug}/lock`);
|
|
1514
|
+
console.log(`Locked "${slug}" \u2014 direct writes now require approval (see \`vaultic env proposals ${slug}\`).`);
|
|
1515
|
+
}
|
|
1516
|
+
async function envUnlockCommand(slug) {
|
|
1517
|
+
const cfg = requireProjectConfig();
|
|
1518
|
+
const api = new ApiClient();
|
|
1519
|
+
await api.post(`${projectBase(cfg.workspace, cfg.project)}/environments/${slug}/unlock`);
|
|
1520
|
+
console.log(`Unlocked "${slug}" \u2014 direct writes are allowed again.`);
|
|
1521
|
+
}
|
|
1522
|
+
async function envProposalsCommand(slug, opts = {}) {
|
|
1523
|
+
const cfg = requireProjectConfig();
|
|
1524
|
+
const api = new ApiClient();
|
|
1525
|
+
const status = opts.status ?? "pending";
|
|
1526
|
+
const proposals = await api.get(
|
|
1527
|
+
`${projectBase(cfg.workspace, cfg.project)}/environments/${slug}/proposals?status=${status}`
|
|
1528
|
+
);
|
|
1529
|
+
if (proposals.length === 0) {
|
|
1530
|
+
console.log(`No ${status} proposals for "${slug}".`);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
for (const p of proposals) {
|
|
1534
|
+
console.log(`${p.id} ${p.type.padEnd(6)} ${p.key.padEnd(30)} ${p.status} ${p.createdAt}`);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
async function envApproveCommand(slug, proposalId) {
|
|
1538
|
+
const cfg = requireProjectConfig();
|
|
1539
|
+
const api = new ApiClient();
|
|
1540
|
+
await api.post(
|
|
1541
|
+
`${projectBase(cfg.workspace, cfg.project)}/environments/${slug}/proposals/${proposalId}/approve`
|
|
1542
|
+
);
|
|
1543
|
+
console.log(`Approved proposal ${proposalId} \u2014 change applied to "${slug}".`);
|
|
1544
|
+
}
|
|
1545
|
+
async function envRejectCommand(slug, proposalId) {
|
|
1546
|
+
const cfg = requireProjectConfig();
|
|
1547
|
+
const api = new ApiClient();
|
|
1548
|
+
await api.post(
|
|
1549
|
+
`${projectBase(cfg.workspace, cfg.project)}/environments/${slug}/proposals/${proposalId}/reject`
|
|
1550
|
+
);
|
|
1551
|
+
console.log(`Rejected proposal ${proposalId}.`);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/commands/tokens.ts
|
|
1555
|
+
async function tokensCreateCommand(name, opts) {
|
|
1556
|
+
const cfg = requireProjectConfig();
|
|
1557
|
+
const api = new ApiClient();
|
|
1558
|
+
const expiresInDays = opts.expires ? parseDays(opts.expires) : void 0;
|
|
1559
|
+
const token = await api.post(`/workspaces/${cfg.workspace}/tokens`, {
|
|
1560
|
+
name,
|
|
1561
|
+
projectSlug: cfg.project,
|
|
1562
|
+
environmentSlug: opts.env,
|
|
1563
|
+
access: opts.readOnly ? "read" : "write",
|
|
1564
|
+
expiresInDays
|
|
1565
|
+
});
|
|
1566
|
+
console.log(`Created token "${name}" (${token.access}):`);
|
|
1567
|
+
console.log(token.token);
|
|
1568
|
+
console.log("\nStore this now \u2014 it will not be shown again.");
|
|
1569
|
+
}
|
|
1570
|
+
async function tokensListCommand() {
|
|
1571
|
+
const cfg = requireProjectConfig();
|
|
1572
|
+
const api = new ApiClient();
|
|
1573
|
+
const tokens2 = await api.get(`/workspaces/${cfg.workspace}/tokens`);
|
|
1574
|
+
for (const t of tokens2) {
|
|
1575
|
+
const status = t.revokedAt ? "revoked" : t.expiresAt && new Date(t.expiresAt) < /* @__PURE__ */ new Date() ? "expired" : "active";
|
|
1576
|
+
console.log(`${t.id} ${t.name} ${t.access} ${status}`);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function tokensRevokeCommand(tokenId) {
|
|
1580
|
+
const cfg = requireProjectConfig();
|
|
1581
|
+
const api = new ApiClient();
|
|
1582
|
+
await api.post(`/workspaces/${cfg.workspace}/tokens/${tokenId}/revoke`);
|
|
1583
|
+
console.log(`Revoked token ${tokenId}`);
|
|
1584
|
+
}
|
|
1585
|
+
function parseDays(expr) {
|
|
1586
|
+
const match = expr.match(/^(\d+)d?$/);
|
|
1587
|
+
if (!match) throw new Error(`Invalid --expires value "${expr}", expected e.g. "90d"`);
|
|
1588
|
+
return Number(match[1]);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/commands/workspace.ts
|
|
1592
|
+
function webUrl() {
|
|
1593
|
+
return process.env.VAULTIC_WEB_URL ?? "http://localhost:5173";
|
|
1594
|
+
}
|
|
1595
|
+
async function workspaceInviteCommand(email, opts) {
|
|
1596
|
+
const cfg = requireProjectConfig();
|
|
1597
|
+
const api = new ApiClient();
|
|
1598
|
+
const invite = await api.post(`/workspaces/${cfg.workspace}/invites`, {
|
|
1599
|
+
email,
|
|
1600
|
+
role: opts.role ?? "developer"
|
|
1601
|
+
});
|
|
1602
|
+
console.log(`Invited ${email} as ${invite.role}. Share this link:`);
|
|
1603
|
+
console.log(`${webUrl()}/accept-invite/${invite.token}`);
|
|
1604
|
+
if (invite.expiresAt) console.log(`(expires ${invite.expiresAt})`);
|
|
1605
|
+
}
|
|
1606
|
+
async function workspaceInvitesCommand() {
|
|
1607
|
+
const cfg = requireProjectConfig();
|
|
1608
|
+
const api = new ApiClient();
|
|
1609
|
+
const invites = await api.get(`/workspaces/${cfg.workspace}/invites`);
|
|
1610
|
+
if (invites.length === 0) {
|
|
1611
|
+
console.log("No pending invites.");
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
for (const i of invites) {
|
|
1615
|
+
console.log(`${i.id} ${i.email} ${i.role} invited by ${i.invitedByEmail ?? "?"} expires ${i.expiresAt ?? "never"}`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async function workspaceRevokeInviteCommand(inviteId) {
|
|
1619
|
+
const cfg = requireProjectConfig();
|
|
1620
|
+
const api = new ApiClient();
|
|
1621
|
+
await api.post(`/workspaces/${cfg.workspace}/invites/${inviteId}/revoke`);
|
|
1622
|
+
console.log(`Revoked invite ${inviteId}`);
|
|
1623
|
+
}
|
|
1624
|
+
async function workspaceMembersCommand() {
|
|
1625
|
+
const cfg = requireProjectConfig();
|
|
1626
|
+
const api = new ApiClient();
|
|
1627
|
+
const members = await api.get(`/workspaces/${cfg.workspace}/members`);
|
|
1628
|
+
for (const m of members) {
|
|
1629
|
+
console.log(`${m.id} ${m.name} <${m.email}> ${m.role}`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
async function findMemberByEmail(api, workspace2, email) {
|
|
1633
|
+
const members = await api.get(`/workspaces/${workspace2}/members`);
|
|
1634
|
+
const member = members.find((m) => m.email.toLowerCase() === email.toLowerCase());
|
|
1635
|
+
if (!member) {
|
|
1636
|
+
console.error(`No member with email ${email} in this workspace.`);
|
|
1637
|
+
process.exit(1);
|
|
1638
|
+
}
|
|
1639
|
+
return member;
|
|
1640
|
+
}
|
|
1641
|
+
async function workspaceSetRoleCommand(email, role) {
|
|
1642
|
+
const cfg = requireProjectConfig();
|
|
1643
|
+
const api = new ApiClient();
|
|
1644
|
+
const member = await findMemberByEmail(api, cfg.workspace, email);
|
|
1645
|
+
const updated = await api.patch(`/workspaces/${cfg.workspace}/members/${member.id}`, { role });
|
|
1646
|
+
console.log(`${email} is now ${updated.role}`);
|
|
1647
|
+
}
|
|
1648
|
+
async function workspaceRemoveMemberCommand(email) {
|
|
1649
|
+
const cfg = requireProjectConfig();
|
|
1650
|
+
const api = new ApiClient();
|
|
1651
|
+
const member = await findMemberByEmail(api, cfg.workspace, email);
|
|
1652
|
+
await api.delete(`/workspaces/${cfg.workspace}/members/${member.id}`);
|
|
1653
|
+
console.log(`Removed ${email} from the workspace`);
|
|
1654
|
+
}
|
|
1655
|
+
async function workspaceAcceptInviteCommand(token) {
|
|
1656
|
+
const api = new ApiClient();
|
|
1657
|
+
const result = await api.post(`/invites/${token}/accept`);
|
|
1658
|
+
console.log(`Joined "${result.workspace.name}" (${result.workspace.slug})`);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/commands/access.ts
|
|
1662
|
+
async function accessGrantCommand(email, opts) {
|
|
1663
|
+
const cfg = requireProjectConfig();
|
|
1664
|
+
const api = new ApiClient();
|
|
1665
|
+
const members = await api.get(`/workspaces/${cfg.workspace}/members`);
|
|
1666
|
+
const member = members.find((m) => m.email.toLowerCase() === email.toLowerCase());
|
|
1667
|
+
if (!member) {
|
|
1668
|
+
console.error(`No member with email ${email} in this workspace.`);
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
const grant = await api.post(`/workspaces/${cfg.workspace}/projects/${cfg.project}/access-grants`, {
|
|
1672
|
+
memberId: member.id,
|
|
1673
|
+
environmentSlug: opts.env,
|
|
1674
|
+
role: opts.role ?? "developer"
|
|
1675
|
+
});
|
|
1676
|
+
console.log(
|
|
1677
|
+
`Granted ${email} ${grant.role} on ${cfg.project}${grant.environmentSlug ? `/${grant.environmentSlug}` : " (whole project)"}`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
async function accessListCommand() {
|
|
1681
|
+
const cfg = requireProjectConfig();
|
|
1682
|
+
const api = new ApiClient();
|
|
1683
|
+
const grants = await api.get(`/workspaces/${cfg.workspace}/projects/${cfg.project}/access-grants`);
|
|
1684
|
+
if (grants.length === 0) {
|
|
1685
|
+
console.log("No access grants for this project.");
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
for (const g of grants) {
|
|
1689
|
+
console.log(`${g.id} ${g.memberEmail} ${g.role} ${g.environmentSlug ?? "(whole project)"}`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
async function accessRevokeCommand(grantId) {
|
|
1693
|
+
const cfg = requireProjectConfig();
|
|
1694
|
+
const api = new ApiClient();
|
|
1695
|
+
await api.delete(`/workspaces/${cfg.workspace}/projects/${cfg.project}/access-grants/${grantId}`);
|
|
1696
|
+
console.log(`Revoked grant ${grantId}`);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/commands/share.ts
|
|
1700
|
+
async function shareCommand(key, opts) {
|
|
1701
|
+
const cfg = requireProjectConfig();
|
|
1702
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1703
|
+
const api = new ApiClient();
|
|
1704
|
+
const result = await api.post(`${envBase(cfg, env2)}/secrets/${key}/share`, {
|
|
1705
|
+
expiresIn: opts.expires,
|
|
1706
|
+
maxViews: opts.maxViews ? Number(opts.maxViews) : void 0
|
|
1707
|
+
});
|
|
1708
|
+
console.log(`${api.getServerUrl()}/share/${result.token}`);
|
|
1709
|
+
const limits = [];
|
|
1710
|
+
if (result.expiresAt) limits.push(`expires ${result.expiresAt}`);
|
|
1711
|
+
if (result.maxViews !== null) limits.push(`max ${result.maxViews} view(s)`);
|
|
1712
|
+
console.log(limits.length ? `(${limits.join(", ")})` : "");
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/commands/webhooks.ts
|
|
1716
|
+
async function webhooksCreateCommand(url, opts) {
|
|
1717
|
+
const cfg = requireProjectConfig();
|
|
1718
|
+
const api = new ApiClient();
|
|
1719
|
+
const events = opts.events.split(",").map((e) => e.trim());
|
|
1720
|
+
const result = await api.post(`/workspaces/${cfg.workspace}/webhooks`, {
|
|
1721
|
+
url,
|
|
1722
|
+
events,
|
|
1723
|
+
kind: opts.slack ? "slack" : "generic"
|
|
1724
|
+
});
|
|
1725
|
+
console.log(`Created webhook ${result.id} (${result.kind})`);
|
|
1726
|
+
if (result.secret) {
|
|
1727
|
+
console.log(`Signing secret (shown once \u2014 save it): ${result.secret}`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
async function webhooksListCommand() {
|
|
1731
|
+
const cfg = requireProjectConfig();
|
|
1732
|
+
const api = new ApiClient();
|
|
1733
|
+
const webhooks2 = await api.get(`/workspaces/${cfg.workspace}/webhooks`);
|
|
1734
|
+
for (const w of webhooks2) {
|
|
1735
|
+
console.log(`${w.id} ${w.kind.padEnd(7)} ${w.url} [${w.events.join(", ")}]${w.disabledAt ? " (disabled)" : ""}`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
async function webhooksDeleteCommand(id) {
|
|
1739
|
+
const cfg = requireProjectConfig();
|
|
1740
|
+
const api = new ApiClient();
|
|
1741
|
+
await api.delete(`/workspaces/${cfg.workspace}/webhooks/${id}`);
|
|
1742
|
+
console.log(`Deleted webhook ${id}`);
|
|
1743
|
+
}
|
|
1744
|
+
async function webhooksDeliveriesCommand(id) {
|
|
1745
|
+
const cfg = requireProjectConfig();
|
|
1746
|
+
const api = new ApiClient();
|
|
1747
|
+
const deliveries = await api.get(`/workspaces/${cfg.workspace}/webhooks/${id}/deliveries`);
|
|
1748
|
+
if (deliveries.length === 0) {
|
|
1749
|
+
console.log("No deliveries yet.");
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
for (const d of deliveries) {
|
|
1753
|
+
console.log(
|
|
1754
|
+
`${d.createdAt} ${d.event.padEnd(24)} ${d.success ? "ok" : "FAILED"} status=${d.statusCode ?? "-"} ${d.durationMs}ms${d.error ? ` ${d.error}` : ""}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/integrations/github.ts
|
|
1760
|
+
import sodium from "libsodium-wrappers";
|
|
1761
|
+
var GithubClient = class {
|
|
1762
|
+
token;
|
|
1763
|
+
owner;
|
|
1764
|
+
repo;
|
|
1765
|
+
environment;
|
|
1766
|
+
fetchFn;
|
|
1767
|
+
constructor(opts) {
|
|
1768
|
+
this.token = opts.token;
|
|
1769
|
+
this.owner = opts.owner;
|
|
1770
|
+
this.repo = opts.repo;
|
|
1771
|
+
this.environment = opts.environment;
|
|
1772
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
1773
|
+
}
|
|
1774
|
+
base() {
|
|
1775
|
+
return this.environment ? `https://api.github.com/repos/${this.owner}/${this.repo}/environments/${this.environment}/secrets` : `https://api.github.com/repos/${this.owner}/${this.repo}/actions/secrets`;
|
|
1776
|
+
}
|
|
1777
|
+
headers() {
|
|
1778
|
+
return {
|
|
1779
|
+
authorization: `Bearer ${this.token}`,
|
|
1780
|
+
accept: "application/vnd.github+json",
|
|
1781
|
+
"content-type": "application/json",
|
|
1782
|
+
"x-github-api-version": "2022-11-28"
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
async getPublicKey() {
|
|
1786
|
+
const res = await this.fetchFn(`${this.base()}/public-key`, { headers: this.headers() });
|
|
1787
|
+
if (!res.ok) throw new Error(`GitHub public key fetch failed: ${res.status} ${await res.text()}`);
|
|
1788
|
+
return res.json();
|
|
1789
|
+
}
|
|
1790
|
+
async listSecretNames() {
|
|
1791
|
+
const res = await this.fetchFn(this.base(), { headers: this.headers() });
|
|
1792
|
+
if (!res.ok) throw new Error(`GitHub list secrets failed: ${res.status} ${await res.text()}`);
|
|
1793
|
+
const data = await res.json();
|
|
1794
|
+
return (data.secrets ?? []).map((s) => s.name);
|
|
1795
|
+
}
|
|
1796
|
+
async putSecret(name, encryptedValue, keyId) {
|
|
1797
|
+
const res = await this.fetchFn(`${this.base()}/${name}`, {
|
|
1798
|
+
method: "PUT",
|
|
1799
|
+
headers: this.headers(),
|
|
1800
|
+
body: JSON.stringify({ encrypted_value: encryptedValue, key_id: keyId })
|
|
1801
|
+
});
|
|
1802
|
+
if (!res.ok) throw new Error(`GitHub set secret "${name}" failed: ${res.status} ${await res.text()}`);
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
async function encryptForGithub(publicKeyBase64, value) {
|
|
1806
|
+
await sodium.ready;
|
|
1807
|
+
const messageBytes = sodium.from_string(value);
|
|
1808
|
+
const keyBytes = sodium.from_base64(publicKeyBase64, sodium.base64_variants.ORIGINAL);
|
|
1809
|
+
const encryptedBytes = sodium.crypto_box_seal(messageBytes, keyBytes);
|
|
1810
|
+
return sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/integrations/vercel.ts
|
|
1814
|
+
var VercelClient = class {
|
|
1815
|
+
token;
|
|
1816
|
+
projectId;
|
|
1817
|
+
teamId;
|
|
1818
|
+
target;
|
|
1819
|
+
fetchFn;
|
|
1820
|
+
constructor(opts) {
|
|
1821
|
+
this.token = opts.token;
|
|
1822
|
+
this.projectId = opts.projectId;
|
|
1823
|
+
this.teamId = opts.teamId;
|
|
1824
|
+
this.target = opts.target ?? ["production"];
|
|
1825
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
1826
|
+
}
|
|
1827
|
+
query() {
|
|
1828
|
+
return this.teamId ? `?teamId=${encodeURIComponent(this.teamId)}` : "";
|
|
1829
|
+
}
|
|
1830
|
+
headers() {
|
|
1831
|
+
return { authorization: `Bearer ${this.token}`, "content-type": "application/json" };
|
|
1832
|
+
}
|
|
1833
|
+
async listEnvVars() {
|
|
1834
|
+
const res = await this.fetchFn(`https://api.vercel.com/v9/projects/${this.projectId}/env${this.query()}`, {
|
|
1835
|
+
headers: this.headers()
|
|
1836
|
+
});
|
|
1837
|
+
if (!res.ok) throw new Error(`Vercel list env vars failed: ${res.status} ${await res.text()}`);
|
|
1838
|
+
const data = await res.json();
|
|
1839
|
+
return data.envs ?? [];
|
|
1840
|
+
}
|
|
1841
|
+
async createEnvVar(key, value) {
|
|
1842
|
+
const res = await this.fetchFn(`https://api.vercel.com/v10/projects/${this.projectId}/env${this.query()}`, {
|
|
1843
|
+
method: "POST",
|
|
1844
|
+
headers: this.headers(),
|
|
1845
|
+
body: JSON.stringify({ key, value, type: "encrypted", target: this.target })
|
|
1846
|
+
});
|
|
1847
|
+
if (!res.ok) throw new Error(`Vercel create env var "${key}" failed: ${res.status} ${await res.text()}`);
|
|
1848
|
+
}
|
|
1849
|
+
async updateEnvVar(envId, value) {
|
|
1850
|
+
const res = await this.fetchFn(`https://api.vercel.com/v9/projects/${this.projectId}/env/${envId}${this.query()}`, {
|
|
1851
|
+
method: "PATCH",
|
|
1852
|
+
headers: this.headers(),
|
|
1853
|
+
body: JSON.stringify({ value })
|
|
1854
|
+
});
|
|
1855
|
+
if (!res.ok) throw new Error(`Vercel update env var failed: ${res.status} ${await res.text()}`);
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
// src/integrations/sync.ts
|
|
1860
|
+
function planSync(existingKeys, desiredKeys) {
|
|
1861
|
+
const existing = new Set(existingKeys);
|
|
1862
|
+
const toCreate = [];
|
|
1863
|
+
const toUpdate = [];
|
|
1864
|
+
for (const key of desiredKeys) {
|
|
1865
|
+
(existing.has(key) ? toUpdate : toCreate).push(key);
|
|
1866
|
+
}
|
|
1867
|
+
return { toCreate, toUpdate };
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/commands/push.ts
|
|
1871
|
+
async function pushGithubCommand(opts) {
|
|
1872
|
+
const cfg = requireProjectConfig();
|
|
1873
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1874
|
+
const api = new ApiClient();
|
|
1875
|
+
const secrets2 = await api.get(`${envBase(cfg, env2)}/export`);
|
|
1876
|
+
const token = opts.token ?? process.env.GITHUB_TOKEN;
|
|
1877
|
+
if (!token) {
|
|
1878
|
+
console.error("Provide a GitHub PAT via GITHUB_TOKEN or --token (needs `secrets` write scope).");
|
|
1879
|
+
process.exit(1);
|
|
1880
|
+
}
|
|
1881
|
+
const client = new GithubClient({ token, owner: opts.owner, repo: opts.repo, environment: opts.ghEnvironment });
|
|
1882
|
+
const publicKey = await client.getPublicKey();
|
|
1883
|
+
const existing = await client.listSecretNames();
|
|
1884
|
+
const plan = planSync(existing, Object.keys(secrets2));
|
|
1885
|
+
for (const key of [...plan.toCreate, ...plan.toUpdate]) {
|
|
1886
|
+
const encrypted = await encryptForGithub(publicKey.key, secrets2[key]);
|
|
1887
|
+
await client.putSecret(key, encrypted, publicKey.key_id);
|
|
1888
|
+
}
|
|
1889
|
+
const target = opts.ghEnvironment ? `${opts.owner}/${opts.repo} (GitHub environment: ${opts.ghEnvironment})` : `${opts.owner}/${opts.repo}`;
|
|
1890
|
+
console.log(`Pushed to GitHub ${target}: ${plan.toCreate.length} new, ${plan.toUpdate.length} updated.`);
|
|
1891
|
+
}
|
|
1892
|
+
async function pushVercelCommand(opts) {
|
|
1893
|
+
const cfg = requireProjectConfig();
|
|
1894
|
+
const env2 = resolveEnvironment(opts.env);
|
|
1895
|
+
const api = new ApiClient();
|
|
1896
|
+
const secrets2 = await api.get(`${envBase(cfg, env2)}/export`);
|
|
1897
|
+
const token = opts.token ?? process.env.VERCEL_TOKEN;
|
|
1898
|
+
if (!token) {
|
|
1899
|
+
console.error("Provide a Vercel token via VERCEL_TOKEN or --token.");
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
const client = new VercelClient({
|
|
1903
|
+
token,
|
|
1904
|
+
projectId: opts.project,
|
|
1905
|
+
teamId: opts.team,
|
|
1906
|
+
target: opts.target ? [opts.target] : void 0
|
|
1907
|
+
});
|
|
1908
|
+
const existingVars = await client.listEnvVars();
|
|
1909
|
+
const existingByKey = new Map(existingVars.map((v) => [v.key, v]));
|
|
1910
|
+
const plan = planSync(existingVars.map((v) => v.key), Object.keys(secrets2));
|
|
1911
|
+
for (const key of plan.toCreate) await client.createEnvVar(key, secrets2[key]);
|
|
1912
|
+
for (const key of plan.toUpdate) await client.updateEnvVar(existingByKey.get(key).id, secrets2[key]);
|
|
1913
|
+
console.log(`Pushed to Vercel project ${opts.project}: ${plan.toCreate.length} new, ${plan.toUpdate.length} updated.`);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/commands/git-integration.ts
|
|
1917
|
+
function base(workspace2, project) {
|
|
1918
|
+
return `/workspaces/${workspace2}/projects/${project}/git-integration`;
|
|
1919
|
+
}
|
|
1920
|
+
async function gitIntegrationSetupCommand(opts) {
|
|
1921
|
+
const cfg = requireProjectConfig();
|
|
1922
|
+
const api = new ApiClient();
|
|
1923
|
+
const integration = await api.post(base(cfg.workspace, cfg.project), {
|
|
1924
|
+
owner: opts.owner,
|
|
1925
|
+
repo: opts.repo,
|
|
1926
|
+
branchPattern: opts.branchPattern,
|
|
1927
|
+
previewEnvironmentTemplate: opts.template,
|
|
1928
|
+
defaultTtlDays: opts.ttlDays ? Number(opts.ttlDays) : void 0
|
|
1929
|
+
});
|
|
1930
|
+
console.log(`Git integration configured for ${integration.owner}/${integration.repo}.`);
|
|
1931
|
+
console.log(``);
|
|
1932
|
+
console.log(`On GitHub: Settings -> Webhooks -> Add webhook`);
|
|
1933
|
+
console.log(` Payload URL: ${integration.webhookUrl}`);
|
|
1934
|
+
console.log(` Content type: application/json`);
|
|
1935
|
+
console.log(` Secret: ${integration.webhookSecret}`);
|
|
1936
|
+
console.log(` Events: "Branches or tags" (push) and "Branch or tag deletion" (delete)`);
|
|
1937
|
+
console.log(``);
|
|
1938
|
+
console.log(
|
|
1939
|
+
`Pushes to branches matching "${integration.branchPattern}" will spawn/renew a preview environment (auto-expires after ${integration.defaultTtlDays}d of inactivity); deleting the branch tears it down immediately.`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
async function gitIntegrationShowCommand() {
|
|
1943
|
+
const cfg = requireProjectConfig();
|
|
1944
|
+
const api = new ApiClient();
|
|
1945
|
+
const integration = await api.get(base(cfg.workspace, cfg.project));
|
|
1946
|
+
console.log(JSON.stringify(integration, null, 2));
|
|
1947
|
+
}
|
|
1948
|
+
async function gitIntegrationRemoveCommand() {
|
|
1949
|
+
const cfg = requireProjectConfig();
|
|
1950
|
+
const api = new ApiClient();
|
|
1951
|
+
await api.delete(base(cfg.workspace, cfg.project));
|
|
1952
|
+
console.log("Git integration removed.");
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/index.ts
|
|
1956
|
+
var program = new Command();
|
|
1957
|
+
program.name("vaultic").description("Vaultic CLI").version("0.1.0");
|
|
1958
|
+
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));
|
|
1960
|
+
program.command("logout").description("Clear stored credentials").action(wrap(logoutCommand));
|
|
1961
|
+
program.command("whoami").description("Show the current logged-in user").action(wrap(whoamiCommand));
|
|
1962
|
+
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)));
|
|
1963
|
+
program.command("export").description("Generate a local secrets file from the server").option("-e, --env <environment>", "environment to use").option("--format <format>", "dotenv | json | yaml | shell").option("--path <path>", "output file path").option("--no-header", "omit the generated-by header/checksum").option("--check", "fail if the local file is stale vs the server (CI mode)").action(wrap(exportCommand));
|
|
1964
|
+
program.command("import").description("Import an existing .env or JSON file into Vaultic").argument("<file>", "path to .env or JSON file").option("-e, --env <environment>", "environment to import into").option("--prefix <prefix>", "prefix to prepend to every imported key").option("--dry-run", "show what would be created/changed without writing").option("--from-json", "treat the file as JSON instead of dotenv").option("--overwrite", "overwrite existing keys instead of skipping them").action(wrap(importCommand));
|
|
1965
|
+
program.command("status").description("Compare local secrets file vs the server").option("-e, --env <environment>").action(wrap(statusCommand));
|
|
1966
|
+
program.command("sync").description("Pull server -> regenerate local secrets file (runs post_sync hook)").option("-e, --env <environment>").action(wrap(syncCommand));
|
|
1967
|
+
var secrets = program.command("secrets").description("Manage secrets");
|
|
1968
|
+
secrets.command("list").option("-e, --env <environment>").option("--reveal", "show real values instead of masked").option("--json", "machine-readable output").action(wrap(secretsListCommand));
|
|
1969
|
+
secrets.command("get").argument("<key>").option("-e, --env <environment>").option("--reveal", "show the real value instead of masked").option("--json", "machine-readable output").option("--previous", "fetch the pre-rotation value instead of the current one (see 'secrets rotate')").action(wrap(secretsGetCommand));
|
|
1970
|
+
secrets.command("set").argument("<key>").argument("[value]", "value, or '-' to read from stdin (omit entirely to update only --expires-*/--remind-every)").option("-e, --env <environment>").option("--from-file <path>", "read the value from a file").option("--expires-in <duration>", 'set an expiry relative to now, e.g. "90d", "12h"').option("--expires-at <date>", "set an absolute expiry date (ISO or any Date-parseable string)").option("--remind-every <duration>", 'set a rotation-reminder cadence, e.g. "30d"').option("--clear-expiry", "remove this secret's expiry date").option("--clear-remind", "remove this secret's rotation reminder").action(wrap(secretsSetCommand));
|
|
1971
|
+
secrets.command("delete").argument("<key>").option("-e, --env <environment>").action(wrap(secretsDeleteCommand));
|
|
1972
|
+
secrets.command("history").argument("<key>").option("-e, --env <environment>").option("--reveal").action(wrap(secretsHistoryCommand));
|
|
1973
|
+
secrets.command("rollback").argument("<key>").requiredOption("--version <version>").option("-e, --env <environment>").action(wrap(secretsRollbackCommand));
|
|
1974
|
+
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 | stripe | aws-iam)").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
|
+
program.command("rotation-providers").description("List available `secrets rotate --provider` plugins").action(wrap(rotationProvidersCommand));
|
|
1977
|
+
var override = secrets.command("override").description("Manage your personal, server-side value overrides");
|
|
1978
|
+
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));
|
|
1979
|
+
override.command("clear").argument("<key>").option("-e, --env <environment>").action(wrap(secretsOverrideClearCommand));
|
|
1980
|
+
var env = program.command("env").description("Manage environments");
|
|
1981
|
+
env.command("list").action(wrap(envListCommand));
|
|
1982
|
+
env.command("create").argument("<slug>").option("--inherits <environment>", "inherit unset keys from this environment").option("--ephemeral", "flag as an ephemeral/preview environment (needs --ttl-days or --expires-at)").option("--ttl-days <days>", "auto-expire this many days from now").option("--expires-at <iso>", "auto-expire at this exact ISO timestamp").action(wrap(envCreateCommand));
|
|
1983
|
+
env.command("duplicate").argument("<source>").argument("<target>").option("--keys-only", "copy keys only, values left blank").option("--link", "link mode: inherit from source instead of copying values").action(wrap(envDuplicateCommand));
|
|
1984
|
+
env.command("diff").argument("<from>").argument("<to>").option("--reveal").action(wrap(envDiffCommand));
|
|
1985
|
+
env.command("promote").argument("<from>").argument("<to>").option("--yes", "skip the confirmation prompt").action(wrap(envPromoteCommand));
|
|
1986
|
+
env.command("lock").argument("<slug>").action(wrap(envLockCommand));
|
|
1987
|
+
env.command("unlock").argument("<slug>").action(wrap(envUnlockCommand));
|
|
1988
|
+
env.command("proposals").argument("<slug>").option("--status <status>", "pending | approved | rejected", "pending").action(wrap(envProposalsCommand));
|
|
1989
|
+
env.command("approve").argument("<slug>").argument("<proposalId>").action(wrap(envApproveCommand));
|
|
1990
|
+
env.command("reject").argument("<slug>").argument("<proposalId>").action(wrap(envRejectCommand));
|
|
1991
|
+
var workspace = program.command("workspace").description("Manage workspace members and invites");
|
|
1992
|
+
workspace.command("invite").argument("<email>").option("-r, --role <role>", '"admin" | "developer" | "read-only"', "developer").action(wrap((email, opts) => workspaceInviteCommand(email, opts)));
|
|
1993
|
+
workspace.command("invites").description("List pending invites").action(wrap(workspaceInvitesCommand));
|
|
1994
|
+
workspace.command("revoke-invite").argument("<inviteId>").action(wrap(workspaceRevokeInviteCommand));
|
|
1995
|
+
workspace.command("members").description("List workspace members").action(wrap(workspaceMembersCommand));
|
|
1996
|
+
workspace.command("set-role").argument("<email>").argument("<role>", '"owner" | "admin" | "developer" | "read-only"').action(wrap((email, role) => workspaceSetRoleCommand(email, role)));
|
|
1997
|
+
workspace.command("remove-member").argument("<email>").action(wrap(workspaceRemoveMemberCommand));
|
|
1998
|
+
workspace.command("accept-invite").argument("<token>").description("Redeem an invite link as the currently logged-in user").action(wrap(workspaceAcceptInviteCommand));
|
|
1999
|
+
var access = program.command("access").description("Manage per-project/per-environment access grants for the current project");
|
|
2000
|
+
access.command("grant").argument("<email>").option("-r, --role <role>", '"developer" | "read-only"', "developer").option("-e, --env <environment>", "scope the grant to a single environment (default: whole project)").action(wrap((email, opts) => accessGrantCommand(email, opts)));
|
|
2001
|
+
access.command("list").description("List access grants for the current project").action(wrap(accessListCommand));
|
|
2002
|
+
access.command("revoke").argument("<grantId>").action(wrap(accessRevokeCommand));
|
|
2003
|
+
var tokens = program.command("tokens").description("Manage service tokens");
|
|
2004
|
+
tokens.command("create").argument("<name>").option("-e, --env <environment>", "scope to a single environment").option("--read-only", "read-only token (default is read/write)").option("--expires <days>", 'e.g. "90d"').action(wrap(tokensCreateCommand));
|
|
2005
|
+
tokens.command("list").action(wrap(tokensListCommand));
|
|
2006
|
+
tokens.command("revoke").argument("<tokenId>").action(wrap(tokensRevokeCommand));
|
|
2007
|
+
program.command("share").description("Generate a one-time/limited-view link to a secret's current value").argument("<key>").option("-e, --env <environment>").option("--expires <duration>", 'e.g. "1h", "30m", "2d"').option("--max-views <n>", "number of times the link can be viewed").action(wrap(shareCommand));
|
|
2008
|
+
var webhooks = program.command("webhooks").description("Manage workspace webhook subscriptions");
|
|
2009
|
+
webhooks.command("create").argument("<url>").requiredOption("--events <events>", "comma-separated event names (e.g. secret.created,secret.updated)").option("--slack", "format deliveries as a Slack message instead of signed JSON").action(wrap(webhooksCreateCommand));
|
|
2010
|
+
webhooks.command("list").action(wrap(webhooksListCommand));
|
|
2011
|
+
webhooks.command("delete").argument("<id>").action(wrap(webhooksDeleteCommand));
|
|
2012
|
+
webhooks.command("deliveries").argument("<id>").action(wrap(webhooksDeliveriesCommand));
|
|
2013
|
+
var push = program.command("push").description("Sync an environment's secrets outward to a third-party target");
|
|
2014
|
+
push.command("github").description("Push to GitHub Actions repo (or repo-environment) secrets").option("-e, --env <environment>").requiredOption("--owner <owner>", "GitHub org/user").requiredOption("--repo <repo>", "GitHub repo name").option("-ge, --gh-environment <name>", "push to a GitHub *Environment*'s secrets instead of repo-level").option("--token <token>", "GitHub PAT (defaults to $GITHUB_TOKEN)").action(wrap(pushGithubCommand));
|
|
2015
|
+
push.command("vercel").description("Push to a Vercel project's environment variables").option("-e, --env <environment>").requiredOption("--project <projectId>", "Vercel project ID").option("--team <teamId>", "Vercel team ID, for team-owned projects").option("--target <target>", 'Vercel target: "production" | "preview" | "development"').option("--token <token>", "Vercel token (defaults to $VERCEL_TOKEN)").action(wrap(pushVercelCommand));
|
|
2016
|
+
var gitIntegration = program.command("git-integration").description("Configure the inbound Git webhook that spawns/tears down ephemeral preview environments");
|
|
2017
|
+
gitIntegration.command("setup").description("Configure (or reconfigure) this project's Git integration; prints the webhook URL + secret to set on GitHub").requiredOption("--owner <owner>", "GitHub org/user").requiredOption("--repo <repo>", "GitHub repo name").option("--branch-pattern <pattern>", 'glob with one "*", e.g. "preview/*" (default)').option("--template <environment>", "environment new preview environments inherit unset keys from").option("--ttl-days <days>", "days a preview environment lives before auto-expiring (default 7)").action(wrap(gitIntegrationSetupCommand));
|
|
2018
|
+
gitIntegration.command("show").description("Show the current Git integration config").action(wrap(gitIntegrationShowCommand));
|
|
2019
|
+
gitIntegration.command("remove").description("Remove the Git integration").action(wrap(gitIntegrationRemoveCommand));
|
|
2020
|
+
function wrap(fn) {
|
|
2021
|
+
return async (...args) => {
|
|
2022
|
+
try {
|
|
2023
|
+
await fn(...args);
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
if (err instanceof ApiError) {
|
|
2026
|
+
console.error(`Error: ${err.message}`);
|
|
2027
|
+
process.exit(err.status === 401 ? 2 : 1);
|
|
2028
|
+
}
|
|
2029
|
+
console.error(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
2030
|
+
process.exit(1);
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
program.parseAsync(process.argv);
|