@earthlink/dotvault 0.13.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 ADDED
@@ -0,0 +1,2326 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { realpathSync } from "fs";
5
+ import { basename as basename6 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { createRequire } from "module";
8
+ import { Command } from "commander";
9
+
10
+ // ../shared/dist/api-types.js
11
+ function isAuthChallenge(r) {
12
+ return "challenge" in r;
13
+ }
14
+ var GLOBAL_ALL_ENV = "_all";
15
+ var FILE_SLUG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
16
+ function isValidFileSlug(value) {
17
+ if (value.length === 0 || value.length > 64)
18
+ return false;
19
+ if (value.includes(".."))
20
+ return false;
21
+ return FILE_SLUG_PATTERN.test(value);
22
+ }
23
+ function fileNameToSlug(fileName) {
24
+ const slug = fileName.startsWith(".") ? fileName.slice(1) : fileName;
25
+ if (!isValidFileSlug(slug)) {
26
+ throw new Error(`Invalid env file name: ${JSON.stringify(fileName)}`);
27
+ }
28
+ return slug;
29
+ }
30
+ function slugToFileName(slug) {
31
+ return slug.startsWith(".") ? slug : `.${slug}`;
32
+ }
33
+
34
+ // ../shared/dist/errors.js
35
+ var EnvSyncError = class extends Error {
36
+ code;
37
+ exitCode;
38
+ constructor(code, message, exitCode) {
39
+ super(message);
40
+ this.name = "EnvSyncError";
41
+ this.code = code;
42
+ this.exitCode = exitCode;
43
+ }
44
+ };
45
+ var AuthRequiredError = class extends EnvSyncError {
46
+ constructor(msg = "Not logged in. Run `dotvault login` first.") {
47
+ super("AUTH_REQUIRED", msg, 3);
48
+ this.name = "AuthRequiredError";
49
+ }
50
+ };
51
+ var AuthExpiredError = class extends EnvSyncError {
52
+ constructor(msg = "Session expired. Run `dotvault login`.") {
53
+ super("AUTH_EXPIRED", msg, 3);
54
+ this.name = "AuthExpiredError";
55
+ }
56
+ };
57
+ var InvalidCredentialsError = class extends EnvSyncError {
58
+ constructor(msg = "Invalid email or password.") {
59
+ super("INVALID_CREDENTIALS", msg, 3);
60
+ this.name = "InvalidCredentialsError";
61
+ }
62
+ };
63
+ var PermissionDeniedError = class extends EnvSyncError {
64
+ constructor(project, env) {
65
+ super("PERMISSION_DENIED", `You do not have permission for project '${project}' env '${env}'. Ask an org admin to grant access at https://app.dotvault.io.`, 4);
66
+ this.name = "PermissionDeniedError";
67
+ }
68
+ };
69
+ var ProjectNotFoundError = class extends EnvSyncError {
70
+ constructor(project, env) {
71
+ super("PROJECT_NOT_FOUND", `No secrets found for project '${project}' env '${env}'. Check the names \u2014 they are case-sensitive \u2014 and that the project exists in the admin console.`, 5);
72
+ this.name = "ProjectNotFoundError";
73
+ }
74
+ };
75
+ var InvalidMfaCodeError = class extends EnvSyncError {
76
+ constructor(msg = "Invalid MFA code. Try again.") {
77
+ super("INVALID_MFA_CODE", msg, 3);
78
+ this.name = "InvalidMfaCodeError";
79
+ }
80
+ };
81
+ var MfaSessionExpiredError = class extends EnvSyncError {
82
+ constructor(msg = "MFA session expired. Run `dotvault login` again.") {
83
+ super("MFA_SESSION_EXPIRED", msg, 3);
84
+ this.name = "MfaSessionExpiredError";
85
+ }
86
+ };
87
+ var NetworkError = class extends EnvSyncError {
88
+ constructor(msg) {
89
+ super("NETWORK_ERROR", msg, 2);
90
+ this.name = "NetworkError";
91
+ }
92
+ };
93
+ var ValidationError = class extends EnvSyncError {
94
+ constructor(msg) {
95
+ super("VALIDATION_ERROR", msg, 6);
96
+ this.name = "ValidationError";
97
+ }
98
+ };
99
+ function isEnvSyncError(value) {
100
+ return value instanceof EnvSyncError;
101
+ }
102
+
103
+ // ../shared/dist/capabilities.js
104
+ var ALL_CAPABILITIES = [
105
+ "secret.versioning",
106
+ "injection.run",
107
+ "cli.write",
108
+ "sdk.access",
109
+ "integration.sync",
110
+ "integration.webhook",
111
+ "dynamic.secrets",
112
+ "secret.rotation",
113
+ "secret.sharing",
114
+ "sso.saml_oidc",
115
+ "scim.provisioning",
116
+ "approval.workflow",
117
+ "audit.search",
118
+ "audit.export",
119
+ "audit.siem_stream",
120
+ "config.inheritance"
121
+ ];
122
+ var DEFAULT_PLAN_CAPABILITIES = {
123
+ free: ["secret.versioning", "injection.run", "cli.write", "sdk.access"],
124
+ team: [
125
+ "secret.versioning",
126
+ "injection.run",
127
+ "cli.write",
128
+ "sdk.access",
129
+ "integration.sync",
130
+ "integration.webhook",
131
+ "secret.sharing",
132
+ "audit.search",
133
+ "config.inheritance"
134
+ ],
135
+ business: [
136
+ "secret.versioning",
137
+ "injection.run",
138
+ "cli.write",
139
+ "sdk.access",
140
+ "integration.sync",
141
+ "integration.webhook",
142
+ "secret.sharing",
143
+ "audit.search",
144
+ "config.inheritance",
145
+ "approval.workflow",
146
+ "dynamic.secrets",
147
+ "secret.rotation",
148
+ "audit.export",
149
+ "sso.saml_oidc"
150
+ ],
151
+ enterprise: [...ALL_CAPABILITIES]
152
+ };
153
+ var DEFAULT_PLAN_LIMITS = {
154
+ free: {
155
+ max_users: 10,
156
+ max_orgs: 1,
157
+ max_projects: 5,
158
+ max_keys: 50,
159
+ max_teams: null
160
+ },
161
+ team: {
162
+ max_users: 100,
163
+ max_orgs: 3,
164
+ max_projects: 50,
165
+ max_keys: 500,
166
+ max_teams: 10
167
+ },
168
+ business: {
169
+ max_users: 500,
170
+ max_orgs: null,
171
+ max_projects: null,
172
+ max_keys: null,
173
+ max_teams: null
174
+ },
175
+ enterprise: {
176
+ max_users: null,
177
+ max_orgs: null,
178
+ max_projects: null,
179
+ max_keys: null,
180
+ max_teams: null
181
+ }
182
+ };
183
+ var DEFAULT_PLAN_ALLOWED_ROLES = {
184
+ free: ["member", "org_admin"],
185
+ team: ["member", "team_admin", "org_admin"],
186
+ business: ["member", "team_admin", "org_admin"],
187
+ enterprise: ["member", "team_admin", "org_admin", "platform_admin"]
188
+ };
189
+ var DEFAULT_PLAN_CATALOG = {
190
+ free: {
191
+ plan: "free",
192
+ capabilities: [...DEFAULT_PLAN_CAPABILITIES.free],
193
+ limits: DEFAULT_PLAN_LIMITS.free,
194
+ allowed_roles: DEFAULT_PLAN_ALLOWED_ROLES.free,
195
+ version: 1,
196
+ updated_at: "1970-01-01T00:00:00.000Z",
197
+ updated_by: "system"
198
+ },
199
+ team: {
200
+ plan: "team",
201
+ capabilities: [...DEFAULT_PLAN_CAPABILITIES.team],
202
+ limits: DEFAULT_PLAN_LIMITS.team,
203
+ allowed_roles: DEFAULT_PLAN_ALLOWED_ROLES.team,
204
+ version: 1,
205
+ updated_at: "1970-01-01T00:00:00.000Z",
206
+ updated_by: "system"
207
+ },
208
+ business: {
209
+ plan: "business",
210
+ capabilities: [...DEFAULT_PLAN_CAPABILITIES.business],
211
+ limits: DEFAULT_PLAN_LIMITS.business,
212
+ allowed_roles: DEFAULT_PLAN_ALLOWED_ROLES.business,
213
+ version: 1,
214
+ updated_at: "1970-01-01T00:00:00.000Z",
215
+ updated_by: "system"
216
+ },
217
+ enterprise: {
218
+ plan: "enterprise",
219
+ capabilities: [...DEFAULT_PLAN_CAPABILITIES.enterprise],
220
+ limits: DEFAULT_PLAN_LIMITS.enterprise,
221
+ allowed_roles: DEFAULT_PLAN_ALLOWED_ROLES.enterprise,
222
+ version: 1,
223
+ updated_at: "1970-01-01T00:00:00.000Z",
224
+ updated_by: "system"
225
+ }
226
+ };
227
+
228
+ // src/config.ts
229
+ import { readFile } from "fs/promises";
230
+ import { resolve } from "path";
231
+
232
+ // src/env-vars.ts
233
+ function resolveServiceToken(env = process.env) {
234
+ return env.DOTVAULT_TOKEN ?? env.ENV_SYNC_TOKEN;
235
+ }
236
+ function resolveAdminServiceToken(env = process.env) {
237
+ return env.DOTVAULT_ADMIN_TOKEN ?? env.ENV_SYNC_ADMIN_TOKEN;
238
+ }
239
+ function resolveApiUrlOverride(env = process.env) {
240
+ return env.DOTVAULT_API_URL ?? env.ENV_SYNC_API_URL;
241
+ }
242
+
243
+ // src/config.ts
244
+ async function loadProjectConfig(cwd = process.cwd()) {
245
+ const path = resolve(cwd, "project-config.json");
246
+ const cfg = await loadProjectConfigIfPresent(cwd);
247
+ if (cfg === null) {
248
+ throw new ValidationError(
249
+ `No project-config.json found at ${path}. Run \`env-sync init\` here, or run env-sync from a directory that has one.`
250
+ );
251
+ }
252
+ return cfg;
253
+ }
254
+ async function loadProjectConfigIfPresent(cwd = process.cwd()) {
255
+ const path = resolve(cwd, "project-config.json");
256
+ let raw;
257
+ try {
258
+ raw = await readFile(path, "utf8");
259
+ } catch (err) {
260
+ const e = err;
261
+ if (e.code === "ENOENT") return null;
262
+ throw new ValidationError(
263
+ `Could not read project-config.json at ${path}: ${err.message}`
264
+ );
265
+ }
266
+ return parseProjectConfig(raw, path);
267
+ }
268
+ var DEFAULT_API_URL = "https://api.dotvault.io/v1";
269
+ async function resolveApiUrl(cwd = process.cwd()) {
270
+ const override = resolveApiUrlOverride();
271
+ if (override !== void 0 && override.length > 0) return override;
272
+ const cfg = await loadProjectConfigIfPresent(cwd);
273
+ return cfg?.proxy.apiUrl ?? DEFAULT_API_URL;
274
+ }
275
+ function parseProjectConfig(raw, source = "<inline>") {
276
+ let parsed;
277
+ try {
278
+ parsed = JSON.parse(raw);
279
+ } catch (err) {
280
+ throw new ValidationError(`Invalid JSON in ${source}: ${err.message}`);
281
+ }
282
+ const name = parsed.name;
283
+ if (typeof name !== "string" || name.length === 0) {
284
+ throw new ValidationError(`${source}: 'name' is required and must be a non-empty string`);
285
+ }
286
+ const proxyRaw = parsed.setup?.bootstrap?.proxy;
287
+ const apiUrl = proxyRaw?.apiUrl;
288
+ if (typeof apiUrl !== "string" || apiUrl.length === 0) {
289
+ throw new ValidationError(
290
+ `${source}: 'setup.bootstrap.proxy.apiUrl' is required for env-sync CLI`
291
+ );
292
+ }
293
+ const envSyncRaw = parsed.setup?.envSync;
294
+ const filesRaw = envSyncRaw?.files;
295
+ const targetRaw = envSyncRaw?.target;
296
+ let files;
297
+ let target;
298
+ let targetIsDeprecated = false;
299
+ if (Array.isArray(filesRaw) && filesRaw.length > 0) {
300
+ files = [];
301
+ for (const f of filesRaw) {
302
+ if (typeof f !== "string" || f.length === 0) {
303
+ throw new ValidationError(
304
+ `${source}: 'setup.envSync.files' must be a non-empty array of file name strings`
305
+ );
306
+ }
307
+ try {
308
+ fileNameToSlug(f);
309
+ } catch (err) {
310
+ throw new ValidationError(
311
+ `${source}: 'setup.envSync.files' entry ${JSON.stringify(f)} is invalid: ${err.message}`
312
+ );
313
+ }
314
+ files.push(f);
315
+ }
316
+ target = typeof targetRaw === "string" && targetRaw.length > 0 ? targetRaw : files[0];
317
+ } else if (typeof targetRaw === "string" && targetRaw.length > 0) {
318
+ try {
319
+ fileNameToSlug(targetRaw);
320
+ } catch (err) {
321
+ throw new ValidationError(
322
+ `${source}: 'setup.envSync.target' is invalid: ${err.message}`
323
+ );
324
+ }
325
+ files = [targetRaw];
326
+ target = targetRaw;
327
+ targetIsDeprecated = true;
328
+ } else {
329
+ throw new ValidationError(
330
+ `${source}: provide 'setup.envSync.files' (preferred, schema 2.7.0) or 'setup.envSync.target' (deprecated, schema 2.6.0)`
331
+ );
332
+ }
333
+ const defaultEnv = envSyncRaw?.defaultEnv;
334
+ if (defaultEnv !== "development" && defaultEnv !== "staging" && defaultEnv !== "production") {
335
+ throw new ValidationError(
336
+ `${source}: 'setup.envSync.defaultEnv' must be one of development/staging/production`
337
+ );
338
+ }
339
+ const enabledRaw = envSyncRaw?.enabled;
340
+ const enabled = typeof enabledRaw === "boolean" ? enabledRaw : false;
341
+ const cognitoUserPoolId = typeof proxyRaw?.cognitoUserPoolId === "string" ? proxyRaw.cognitoUserPoolId : void 0;
342
+ const cognitoClientId = typeof proxyRaw?.cognitoClientId === "string" ? proxyRaw.cognitoClientId : void 0;
343
+ return {
344
+ name,
345
+ proxy: { apiUrl, cognitoUserPoolId, cognitoClientId },
346
+ envSync: { enabled, files, target, targetIsDeprecated, defaultEnv }
347
+ };
348
+ }
349
+ function resolveEnv(flag, appEnvVar, defaultEnv) {
350
+ return flag ?? appEnvVar ?? defaultEnv;
351
+ }
352
+
353
+ // src/http.ts
354
+ function createHttpClient(apiUrl, fetchImpl = fetch) {
355
+ const base = apiUrl.replace(/\/+$/, "");
356
+ async function post(path, body, token) {
357
+ const res = await callFetch(fetchImpl, `${base}${path}`, {
358
+ method: "POST",
359
+ headers: jsonHeaders(token),
360
+ body: JSON.stringify(body)
361
+ });
362
+ return handleResponse(res);
363
+ }
364
+ return {
365
+ async login(req) {
366
+ return post("/auth/login", req);
367
+ },
368
+ async mfaRespond(req) {
369
+ return post("/auth/mfa-respond", req);
370
+ },
371
+ async refresh(req) {
372
+ return post("/auth/refresh", req);
373
+ },
374
+ async envPull(token, query) {
375
+ const url = new URL(`${base}/env/pull`);
376
+ url.searchParams.set("project", query.project);
377
+ url.searchParams.set("env", query.env);
378
+ url.searchParams.set("file", query.file);
379
+ const res = await callFetch(fetchImpl, url.toString(), {
380
+ method: "GET",
381
+ headers: jsonHeaders(token)
382
+ });
383
+ return handleResponse(res);
384
+ },
385
+ async envPullAll(token, query) {
386
+ const url = new URL(`${base}/env/pull`);
387
+ url.searchParams.set("project", query.project);
388
+ url.searchParams.set("env", query.env);
389
+ const res = await callFetch(fetchImpl, url.toString(), {
390
+ method: "GET",
391
+ headers: jsonHeaders(token)
392
+ });
393
+ return handleResponse(res);
394
+ },
395
+ async envList(token, query) {
396
+ const url = new URL(`${base}/env/list`);
397
+ url.searchParams.set("project", query.project);
398
+ const res = await callFetch(fetchImpl, url.toString(), {
399
+ method: "GET",
400
+ headers: jsonHeaders(token)
401
+ });
402
+ return handleResponse(res);
403
+ },
404
+ async envKeysPut(token, query, body) {
405
+ const url = writeUrl(base, "/env/keys", query);
406
+ const res = await callFetch(fetchImpl, url, {
407
+ method: "PUT",
408
+ headers: jsonHeaders(token),
409
+ body: JSON.stringify(body)
410
+ });
411
+ return handleResponse(res);
412
+ },
413
+ async envKeysDelete(token, query, body) {
414
+ const url = writeUrl(base, "/env/keys", query);
415
+ const res = await callFetch(fetchImpl, url, {
416
+ method: "DELETE",
417
+ headers: jsonHeaders(token),
418
+ body: JSON.stringify(body)
419
+ });
420
+ return handleResponse(res);
421
+ },
422
+ async envVersions(token, query) {
423
+ const url = new URL(`${base}/env/versions`);
424
+ url.searchParams.set("project", query.project);
425
+ url.searchParams.set("env", query.env);
426
+ url.searchParams.set("file", query.file);
427
+ url.searchParams.set("key", query.key);
428
+ const res = await callFetch(fetchImpl, url.toString(), {
429
+ method: "GET",
430
+ headers: jsonHeaders(token)
431
+ });
432
+ return handleResponse(res);
433
+ },
434
+ async envRollback(token, query, body) {
435
+ const url = writeUrl(base, "/env/rollback", query);
436
+ const res = await callFetch(fetchImpl, url, {
437
+ method: "POST",
438
+ headers: jsonHeaders(token),
439
+ body: JSON.stringify(body)
440
+ });
441
+ return handleResponse(res);
442
+ },
443
+ async envFilesPut(token, query, body) {
444
+ const url = new URL(`${base}/env/files`);
445
+ url.searchParams.set("project", query.project);
446
+ url.searchParams.set("env", query.env);
447
+ const res = await callFetch(fetchImpl, url.toString(), {
448
+ method: "PUT",
449
+ headers: jsonHeaders(token),
450
+ body: JSON.stringify(body)
451
+ });
452
+ return handleResponse(res);
453
+ },
454
+ async envFilesGet(token, query) {
455
+ const url = new URL(`${base}/env/files`);
456
+ url.searchParams.set("project", query.project);
457
+ url.searchParams.set("env", query.env);
458
+ const res = await callFetch(fetchImpl, url.toString(), {
459
+ method: "GET",
460
+ headers: jsonHeaders(token)
461
+ });
462
+ return handleResponse(res);
463
+ },
464
+ async envGlobalKeys(token, query, opts) {
465
+ const url = new URL(`${base}/env/global-keys`);
466
+ if (query?.project !== void 0) url.searchParams.set("project", query.project);
467
+ const res = await callFetch(fetchImpl, url.toString(), {
468
+ method: "GET",
469
+ headers: jsonHeaders(token, opts?.orgId)
470
+ });
471
+ return handleResponse(res);
472
+ },
473
+ async envGlobalKeysSet(token, query, body, opts) {
474
+ const url = new URL(`${base}/env/global-keys`);
475
+ if (query.env !== void 0) url.searchParams.set("env", query.env);
476
+ const res = await callFetch(fetchImpl, url.toString(), {
477
+ method: "PUT",
478
+ headers: jsonHeaders(token, opts?.orgId),
479
+ body: JSON.stringify(body)
480
+ });
481
+ return handleResponse(res);
482
+ },
483
+ async envGlobalKeysDelete(token, query, body, opts) {
484
+ const url = new URL(`${base}/env/global-keys`);
485
+ if (query.env !== void 0) url.searchParams.set("env", query.env);
486
+ const res = await callFetch(fetchImpl, url.toString(), {
487
+ method: "DELETE",
488
+ headers: jsonHeaders(token, opts?.orgId),
489
+ body: JSON.stringify(body)
490
+ });
491
+ return handleResponse(res);
492
+ },
493
+ async envGlobalBindingsGet(token, query) {
494
+ const url = new URL(`${base}/env/global-bindings`);
495
+ url.searchParams.set("project", query.project);
496
+ const res = await callFetch(fetchImpl, url.toString(), {
497
+ method: "GET",
498
+ headers: jsonHeaders(token)
499
+ });
500
+ return handleResponse(res);
501
+ },
502
+ async envGlobalBindingsPatch(token, query, body) {
503
+ const url = new URL(`${base}/env/global-bindings`);
504
+ url.searchParams.set("project", query.project);
505
+ const res = await callFetch(fetchImpl, url.toString(), {
506
+ method: "PATCH",
507
+ headers: jsonHeaders(token),
508
+ body: JSON.stringify(body)
509
+ });
510
+ return handleResponse(res);
511
+ },
512
+ async adminTokensList(token, query) {
513
+ const res = await callFetch(
514
+ fetchImpl,
515
+ `${base}/admin/orgs/${encodeURIComponent(query.orgId)}/tokens`,
516
+ { method: "GET", headers: jsonHeaders(token) }
517
+ );
518
+ return handleResponse(res);
519
+ },
520
+ async adminTokensCreate(token, query, body) {
521
+ const res = await callFetch(
522
+ fetchImpl,
523
+ `${base}/admin/orgs/${encodeURIComponent(query.orgId)}/tokens`,
524
+ { method: "POST", headers: jsonHeaders(token), body: JSON.stringify(body) }
525
+ );
526
+ return handleResponse(res);
527
+ },
528
+ async adminTokensRevoke(token, query) {
529
+ const res = await callFetch(
530
+ fetchImpl,
531
+ `${base}/admin/orgs/${encodeURIComponent(query.orgId)}/tokens/${encodeURIComponent(query.tokenId)}`,
532
+ { method: "DELETE", headers: jsonHeaders(token) }
533
+ );
534
+ if (!res.ok) {
535
+ await handleResponse(res);
536
+ }
537
+ },
538
+ async adminProjectsCreate(token, body) {
539
+ const res = await callFetch(fetchImpl, `${base}/admin/projects`, {
540
+ method: "POST",
541
+ headers: jsonHeaders(token),
542
+ body: JSON.stringify(body)
543
+ });
544
+ return handleResponse(res);
545
+ },
546
+ async adminProjectsUpdate(token, projectRef, body) {
547
+ const res = await callFetch(
548
+ fetchImpl,
549
+ `${base}/admin/projects/${encodeURIComponent(projectRef)}`,
550
+ {
551
+ method: "PUT",
552
+ headers: jsonHeaders(token),
553
+ body: JSON.stringify(body)
554
+ }
555
+ );
556
+ return handleResponse(res);
557
+ }
558
+ };
559
+ }
560
+ function writeUrl(base, path, query) {
561
+ const url = new URL(`${base}${path}`);
562
+ url.searchParams.set("project", query.project);
563
+ url.searchParams.set("env", query.env);
564
+ url.searchParams.set("file", query.file);
565
+ return url.toString();
566
+ }
567
+ function jsonHeaders(token, orgId) {
568
+ const h = { "content-type": "application/json" };
569
+ if (token !== void 0) h.authorization = `Bearer ${token}`;
570
+ if (orgId !== void 0 && orgId.length > 0) h["x-org-context"] = orgId;
571
+ return h;
572
+ }
573
+ async function callFetch(fetchImpl, url, init) {
574
+ try {
575
+ return await fetchImpl(url, init);
576
+ } catch (err) {
577
+ const msg = err.message;
578
+ throw new NetworkError(
579
+ `Could not reach ${new URL(url).host} (${msg}). Check your network and the apiUrl in project-config.json.`
580
+ );
581
+ }
582
+ }
583
+ async function handleResponse(res) {
584
+ if (res.ok) {
585
+ return await res.json();
586
+ }
587
+ const body = await res.text();
588
+ let parsed;
589
+ try {
590
+ parsed = JSON.parse(body);
591
+ } catch {
592
+ parsed = void 0;
593
+ }
594
+ const code = parsed?.error ?? `HTTP_${res.status}`;
595
+ const message = parsed?.message ?? `Request failed: HTTP ${res.status}`;
596
+ if (res.status === 400) {
597
+ throw new EnvSyncError(code, message, 1);
598
+ }
599
+ if (res.status === 401) {
600
+ if (code === "INVALID_CREDENTIALS") throw new InvalidCredentialsError(message);
601
+ if (code === "INVALID_MFA_CODE") throw new InvalidMfaCodeError(message);
602
+ if (code === "MFA_SESSION_EXPIRED") throw new MfaSessionExpiredError(message);
603
+ if (code === "AUTH_EXPIRED") throw new AuthExpiredError(message);
604
+ throw new AuthRequiredError(message);
605
+ }
606
+ if (res.status === 403) {
607
+ throw new PermissionDeniedError("unknown", "unknown");
608
+ }
609
+ if (res.status === 404) {
610
+ throw new ProjectNotFoundError("unknown", "unknown");
611
+ }
612
+ throw new EnvSyncError(code, message, 99);
613
+ }
614
+
615
+ // src/keychain.ts
616
+ var SERVICE = "eln-env-sync";
617
+ var keytarInstance;
618
+ async function getKeytar() {
619
+ if (keytarInstance === void 0) {
620
+ const mod = await import("keytar");
621
+ keytarInstance = mod.default ?? mod;
622
+ }
623
+ return keytarInstance;
624
+ }
625
+ async function saveTokens(email, tokens) {
626
+ const k = await getKeytar();
627
+ await k.setPassword(SERVICE, email, JSON.stringify(tokens));
628
+ }
629
+ async function loadTokens(email) {
630
+ const k = await getKeytar();
631
+ const raw = await k.getPassword(SERVICE, email);
632
+ if (raw === null) return null;
633
+ return JSON.parse(raw);
634
+ }
635
+ async function deleteTokens(email) {
636
+ const k = await getKeytar();
637
+ return k.deletePassword(SERVICE, email);
638
+ }
639
+ async function getCurrentUser() {
640
+ const k = await getKeytar();
641
+ const accts = await k.findCredentials(SERVICE);
642
+ return accts.length > 0 ? accts[0]?.account ?? null : null;
643
+ }
644
+
645
+ // src/commands/global.ts
646
+ async function globalKeysCommand(opts = {}) {
647
+ const { http, orgId, authedFetch } = await resolvePool(opts);
648
+ return authedFetch((bearer) => http.envGlobalKeys(bearer, void 0, { orgId }));
649
+ }
650
+ async function globalSetCommand(opts) {
651
+ const { http, orgId, env, authedFetch } = await resolvePoolWithEnv(opts);
652
+ const body = {
653
+ ...opts.keys !== void 0 ? { keys: opts.keys } : {},
654
+ ...opts.descriptions !== void 0 ? { descriptions: opts.descriptions } : {}
655
+ };
656
+ return authedFetch((bearer) => http.envGlobalKeysSet(bearer, { env }, body, { orgId }));
657
+ }
658
+ async function globalDeleteCommand(opts) {
659
+ const { http, orgId, env, authedFetch } = await resolvePoolWithEnv(opts);
660
+ return authedFetch(
661
+ (bearer) => http.envGlobalKeysDelete(bearer, { env }, { keys: opts.keys }, { orgId })
662
+ );
663
+ }
664
+ async function globalDescribeCommand(opts) {
665
+ const { http, orgId, env, authedFetch } = await resolvePoolWithEnv(opts);
666
+ return authedFetch(
667
+ (bearer) => http.envGlobalKeysSet(bearer, { env }, { descriptions: { [opts.key]: opts.text } }, { orgId })
668
+ );
669
+ }
670
+ async function globalStatusCommand(opts = {}) {
671
+ const { http, project, authedFetch } = await resolve2(opts);
672
+ return authedFetch((bearer) => http.envGlobalBindingsGet(bearer, { project }));
673
+ }
674
+ async function globalUseCommand(opts) {
675
+ const { http, project, authedFetch } = await resolve2(opts);
676
+ const entry = opts.alias !== void 0 ? { key: opts.key, alias: opts.alias } : { key: opts.key };
677
+ return authedFetch(
678
+ (bearer) => http.envGlobalBindingsPatch(bearer, { project }, { add: [entry] })
679
+ );
680
+ }
681
+ async function globalDropCommand(opts) {
682
+ const { http, project, authedFetch } = await resolve2(opts);
683
+ return authedFetch(
684
+ (bearer) => http.envGlobalBindingsPatch(bearer, { project }, { remove: [opts.key] })
685
+ );
686
+ }
687
+ async function resolve2(opts) {
688
+ const cfg = await loadProjectConfig(opts.cwd);
689
+ const project = opts.project ?? cfg.name;
690
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
691
+ const authedFetch = await buildAuthedFetch(http);
692
+ return { http, project, authedFetch };
693
+ }
694
+ async function resolvePool(opts) {
695
+ const cfg = await loadProjectConfig(opts.cwd);
696
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
697
+ const authedFetch = await buildAuthedFetch(http);
698
+ return { http, orgId: opts.orgId, authedFetch };
699
+ }
700
+ async function resolvePoolWithEnv(opts) {
701
+ const cfg = await loadProjectConfig(opts.cwd);
702
+ const env = opts.env ?? GLOBAL_ALL_ENV;
703
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
704
+ const authedFetch = await buildAuthedFetch(http);
705
+ return { http, orgId: opts.orgId, env, authedFetch };
706
+ }
707
+ async function buildAuthedFetch(http) {
708
+ const serviceToken = resolveServiceToken();
709
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
710
+ return (call) => call(serviceToken);
711
+ }
712
+ const email = await getCurrentUser();
713
+ if (email === null) throw new AuthRequiredError();
714
+ const tokens = await loadTokens(email);
715
+ if (tokens === null) throw new AuthRequiredError();
716
+ return async (call) => {
717
+ try {
718
+ return await call(tokens.idToken);
719
+ } catch (err) {
720
+ if (!(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
721
+ throw err;
722
+ }
723
+ const refreshed = await http.refresh({ refreshToken: tokens.refreshToken });
724
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
725
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
726
+ return await call(refreshed.idToken);
727
+ }
728
+ };
729
+ }
730
+
731
+ // src/commands/list.ts
732
+ async function listCommand(opts = {}) {
733
+ const cfg = await loadProjectConfig(opts.cwd);
734
+ const project = opts.project ?? cfg.name;
735
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
736
+ const serviceToken = resolveServiceToken();
737
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
738
+ const res2 = await http.envList(serviceToken, { project });
739
+ return { project: res2.project, files: res2.files };
740
+ }
741
+ const email = await getCurrentUser();
742
+ if (email === null) throw new AuthRequiredError();
743
+ const tokens = await loadTokens(email);
744
+ if (tokens === null) throw new AuthRequiredError();
745
+ const res = await fetchWithRefresh(
746
+ http,
747
+ tokens,
748
+ email,
749
+ (idToken) => http.envList(idToken, { project })
750
+ );
751
+ return { project: res.project, files: res.files };
752
+ }
753
+ async function fetchWithRefresh(http, tokens, email, call) {
754
+ try {
755
+ return await call(tokens.idToken);
756
+ } catch (err) {
757
+ if (!(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
758
+ throw err;
759
+ }
760
+ const refreshed = await http.refresh({ refreshToken: tokens.refreshToken });
761
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
762
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
763
+ return await call(refreshed.idToken);
764
+ }
765
+ }
766
+
767
+ // src/prompt.ts
768
+ import { createInterface } from "readline/promises";
769
+ import { stdin, stdout } from "process";
770
+ async function promptText(label) {
771
+ const rl = createInterface({ input: stdin, output: stdout });
772
+ try {
773
+ const answer = await rl.question(label);
774
+ return answer.trim();
775
+ } finally {
776
+ rl.close();
777
+ }
778
+ }
779
+ async function promptPassword(label) {
780
+ return new Promise((resolve8, reject) => {
781
+ stdout.write(label);
782
+ const wasRaw = stdin.isTTY ? stdin.isRaw : false;
783
+ if (stdin.isTTY) stdin.setRawMode(true);
784
+ stdin.resume();
785
+ stdin.setEncoding("utf8");
786
+ let buf = "";
787
+ const onData = (chunk) => {
788
+ for (const ch of chunk) {
789
+ if (ch === "\n" || ch === "\r" || ch === "") {
790
+ cleanup();
791
+ stdout.write("\n");
792
+ resolve8(buf);
793
+ return;
794
+ }
795
+ if (ch === "") {
796
+ cleanup();
797
+ stdout.write("\n");
798
+ reject(new Error("Aborted"));
799
+ return;
800
+ }
801
+ if (ch === "\x7F" || ch === "\b") {
802
+ buf = buf.slice(0, -1);
803
+ continue;
804
+ }
805
+ buf += ch;
806
+ }
807
+ };
808
+ const cleanup = () => {
809
+ stdin.off("data", onData);
810
+ if (stdin.isTTY) stdin.setRawMode(wasRaw);
811
+ stdin.pause();
812
+ };
813
+ stdin.on("data", onData);
814
+ });
815
+ }
816
+
817
+ // src/commands/login.ts
818
+ var MFA_MAX_ATTEMPTS = 3;
819
+ async function loginCommand(opts = {}) {
820
+ const apiUrl = await resolveApiUrl(opts.cwd);
821
+ const email = opts.email ?? await promptText("Email: ");
822
+ const password = await promptPassword("Password: ");
823
+ const http = opts.http ?? createHttpClient(apiUrl);
824
+ const result = await http.login({ email, password });
825
+ let tokens;
826
+ let usedMfa = false;
827
+ if (isAuthChallenge(result)) {
828
+ usedMfa = true;
829
+ tokens = await answerMfa(http, result.session, email, opts.mfaCode);
830
+ } else {
831
+ tokens = result;
832
+ }
833
+ const expiresAt = Math.floor(Date.now() / 1e3) + tokens.expiresIn;
834
+ await saveTokens(email, {
835
+ idToken: tokens.idToken,
836
+ refreshToken: tokens.refreshToken,
837
+ expiresAt
838
+ });
839
+ return { email, expiresAt, mfa: usedMfa };
840
+ }
841
+ async function answerMfa(http, session, email, presetCode) {
842
+ for (let attempt = 1; attempt <= MFA_MAX_ATTEMPTS; attempt += 1) {
843
+ const code = presetCode ?? await promptText("MFA code: ");
844
+ try {
845
+ return await http.mfaRespond({ email, session, code });
846
+ } catch (err) {
847
+ if (err instanceof InvalidMfaCodeError && presetCode === void 0 && attempt < MFA_MAX_ATTEMPTS) {
848
+ process.stderr.write("Invalid MFA code, try again.\n");
849
+ continue;
850
+ }
851
+ throw err;
852
+ }
853
+ }
854
+ throw new InvalidMfaCodeError("Too many invalid MFA attempts.");
855
+ }
856
+
857
+ // src/commands/logout.ts
858
+ async function logoutCommand() {
859
+ const email = await getCurrentUser();
860
+ if (email === null) return null;
861
+ await deleteTokens(email);
862
+ return email;
863
+ }
864
+
865
+ // src/commands/pull.ts
866
+ import { writeFileSync } from "fs";
867
+ import { resolve as resolve3, basename as basename2 } from "path";
868
+
869
+ // src/env-writer.ts
870
+ import { chmod, readFile as readFile2, rename, unlink, writeFile } from "fs/promises";
871
+ import { dirname, basename, join } from "path";
872
+ import { randomBytes } from "crypto";
873
+ var MARKER_BEGIN = "# BEGIN: eln-bootstrap-envsync (managed by env-sync)";
874
+ var MARKER_END = "# END: eln-bootstrap-envsync";
875
+ async function rewriteMarkers(target, keys) {
876
+ const prepared = await prepareRewrite(target, keys);
877
+ await writeTemp(prepared);
878
+ await commitTemp(prepared);
879
+ return prepared.result;
880
+ }
881
+ async function rewriteMarkersMulti(files) {
882
+ const prepared = [];
883
+ try {
884
+ for (const f of files) {
885
+ prepared.push(await prepareRewrite(f.target, f.keys));
886
+ }
887
+ for (const p of prepared) {
888
+ await writeTemp(p);
889
+ }
890
+ } catch (err) {
891
+ await Promise.allSettled(prepared.map((p) => unlink(p.tmp).catch(() => void 0)));
892
+ throw err;
893
+ }
894
+ const results = [];
895
+ for (const p of prepared) {
896
+ await commitTemp(p);
897
+ results.push({
898
+ target: p.target,
899
+ writtenKeys: p.result.writtenKeys,
900
+ hadExistingMarkers: p.result.hadExistingMarkers,
901
+ targetExisted: p.result.targetExisted
902
+ });
903
+ }
904
+ return results;
905
+ }
906
+ async function prepareRewrite(target, keys) {
907
+ let existing = "";
908
+ let targetExisted = true;
909
+ try {
910
+ existing = await readFile2(target, "utf8");
911
+ } catch (err) {
912
+ const code = err.code;
913
+ if (code === "ENOENT") {
914
+ targetExisted = false;
915
+ } else {
916
+ throw err;
917
+ }
918
+ }
919
+ const { outside, hadExistingMarkers } = stripMarkerBlock(existing);
920
+ const block = renderBlock(keys);
921
+ const next = composeFile(outside, block);
922
+ const dir = dirname(target);
923
+ const tmpName = `.${basename(target)}.${randomBytes(6).toString("hex")}.tmp`;
924
+ const tmp = join(dir, tmpName);
925
+ return {
926
+ target,
927
+ tmp,
928
+ content: next,
929
+ result: {
930
+ writtenKeys: Object.keys(keys),
931
+ hadExistingMarkers,
932
+ targetExisted
933
+ }
934
+ };
935
+ }
936
+ async function writeTemp(p) {
937
+ await writeFile(p.tmp, p.content, { mode: 384 });
938
+ }
939
+ async function commitTemp(p) {
940
+ await rename(p.tmp, p.target);
941
+ await chmod(p.target, 384);
942
+ }
943
+ function stripMarkerBlock(content) {
944
+ const beginIdx = content.indexOf(MARKER_BEGIN);
945
+ if (beginIdx === -1) {
946
+ return { outside: content, hadExistingMarkers: false };
947
+ }
948
+ const endIdx = content.indexOf(MARKER_END, beginIdx);
949
+ if (endIdx === -1) {
950
+ return { outside: content, hadExistingMarkers: false };
951
+ }
952
+ const afterEnd = content.indexOf("\n", endIdx);
953
+ const tail = afterEnd === -1 ? "" : content.slice(afterEnd + 1);
954
+ const head = content.slice(0, beginIdx);
955
+ const trimmedHead = head.replace(/\n+$/, "");
956
+ const outside = trimmedHead.length === 0 ? tail : tail.length === 0 ? trimmedHead : `${trimmedHead}
957
+ ${tail}`;
958
+ return { outside, hadExistingMarkers: true };
959
+ }
960
+ function renderBlock(keys) {
961
+ const lines = [MARKER_BEGIN];
962
+ for (const [k, v] of Object.entries(keys)) {
963
+ lines.push(`${k}=${v}`);
964
+ }
965
+ lines.push(MARKER_END);
966
+ return lines.join("\n");
967
+ }
968
+ function composeFile(outside, block) {
969
+ if (outside.length === 0) return `${block}
970
+ `;
971
+ const trimmed = outside.replace(/\n+$/, "");
972
+ return `${trimmed}
973
+
974
+ ${block}
975
+ `;
976
+ }
977
+
978
+ // src/commands/pull.ts
979
+ async function pullCommand(opts = {}) {
980
+ const cfg = await loadProjectConfig(opts.cwd);
981
+ const project = opts.project ?? cfg.name;
982
+ const env = resolveEnv(opts.env, process.env.APP_ENV, cfg.envSync.defaultEnv);
983
+ const cwd = opts.cwd ?? process.cwd();
984
+ const explicitFile = opts.file ?? (opts.target !== void 0 ? basename2(opts.target) : void 0);
985
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
986
+ opts.onProgress?.("authenticating");
987
+ const authedFetch = await buildAuthedFetch2(http);
988
+ if (explicitFile !== void 0) {
989
+ const fileName = slugToFileName(fileNameToSlug(explicitFile));
990
+ opts.onProgress?.("fetching");
991
+ const response2 = await authedFetch(
992
+ (bearer) => http.envPull(bearer, { project, env, file: fileName })
993
+ );
994
+ const target = opts.target !== void 0 ? resolve3(cwd, opts.target) : resolve3(cwd, fileName);
995
+ opts.onProgress?.("writing");
996
+ const result = await rewriteMarkers(target, response2.keys);
997
+ return {
998
+ project: response2.project,
999
+ env: response2.env,
1000
+ files: [
1001
+ {
1002
+ file: response2.file,
1003
+ writtenKeys: result.writtenKeys,
1004
+ target,
1005
+ hadExistingMarkers: result.hadExistingMarkers
1006
+ }
1007
+ ],
1008
+ summary: { filesSynced: 1, filesExcluded: 0, totalKeys: result.writtenKeys.length }
1009
+ };
1010
+ }
1011
+ opts.onProgress?.("fetching");
1012
+ const response = await authedFetch((bearer) => http.envPullAll(bearer, { project, env }));
1013
+ const requests = response.files.map((f) => ({
1014
+ target: resolve3(cwd, f.file),
1015
+ keys: f.keys
1016
+ }));
1017
+ opts.onProgress?.("writing");
1018
+ const writes = await rewriteMarkersMulti(requests);
1019
+ try {
1020
+ const fileResp = await authedFetch(
1021
+ (bearer) => http.envFilesGet(bearer, { project, env })
1022
+ ).catch(() => null);
1023
+ for (const f of fileResp?.files ?? []) {
1024
+ writeFileSync(resolve3(cwd, f.destPath), f.content, { mode: 384 });
1025
+ }
1026
+ } catch {
1027
+ }
1028
+ const files = response.files.map((f, i) => ({
1029
+ file: f.file,
1030
+ writtenKeys: writes[i].writtenKeys,
1031
+ target: writes[i].target,
1032
+ hadExistingMarkers: writes[i].hadExistingMarkers
1033
+ }));
1034
+ const declaredSlugs = new Set(cfg.envSync.files.map((f) => fileNameToSlug(f)));
1035
+ const syncedSlugs = new Set(files.map((f) => fileNameToSlug(f.file)));
1036
+ const filesExcluded = [...declaredSlugs].filter((slug) => !syncedSlugs.has(slug)).length;
1037
+ const totalKeys = files.reduce((sum, f) => sum + f.writtenKeys.length, 0);
1038
+ return {
1039
+ project: response.project,
1040
+ env: response.env,
1041
+ files,
1042
+ summary: { filesSynced: files.length, filesExcluded, totalKeys }
1043
+ };
1044
+ }
1045
+ async function buildAuthedFetch2(http) {
1046
+ const serviceToken = resolveServiceToken();
1047
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
1048
+ return (call) => call(serviceToken);
1049
+ }
1050
+ const email = await getCurrentUser();
1051
+ if (email === null) throw new AuthRequiredError();
1052
+ const tokens = await loadTokens(email);
1053
+ if (tokens === null) throw new AuthRequiredError();
1054
+ return async (call) => {
1055
+ try {
1056
+ return await call(tokens.idToken);
1057
+ } catch (err) {
1058
+ if (!(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
1059
+ throw err;
1060
+ }
1061
+ const refreshed = await http.refresh({ refreshToken: tokens.refreshToken });
1062
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
1063
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
1064
+ return await call(refreshed.idToken);
1065
+ }
1066
+ };
1067
+ }
1068
+
1069
+ // src/commands/run.ts
1070
+ import { spawn as nodeSpawn } from "child_process";
1071
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
1072
+ import { tmpdir } from "os";
1073
+ import { basename as basename3, join as join2, resolve as resolve4 } from "path";
1074
+ async function runCommand(opts) {
1075
+ if (opts.command === void 0 || opts.command.length === 0) {
1076
+ throw new EnvSyncError(
1077
+ "BAD_REQUEST",
1078
+ "run requires a command, e.g. env-sync run -- npm start",
1079
+ 1
1080
+ );
1081
+ }
1082
+ const cfg = await loadProjectConfig(opts.cwd);
1083
+ const project = opts.project ?? cfg.name;
1084
+ const env = resolveEnv(opts.env, process.env.APP_ENV, cfg.envSync.defaultEnv);
1085
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
1086
+ const authedFetch = await buildAuthedFetch3(http);
1087
+ let merged = {};
1088
+ if (opts.file !== void 0) {
1089
+ const fileName = slugToFileName(fileNameToSlug(opts.file));
1090
+ const response = await authedFetch(
1091
+ (bearer) => http.envPull(bearer, { project, env, file: fileName })
1092
+ );
1093
+ merged = { ...response.keys };
1094
+ } else {
1095
+ const response = await authedFetch((bearer) => http.envPullAll(bearer, { project, env }));
1096
+ for (const f of response.files) {
1097
+ merged = { ...merged, ...f.keys };
1098
+ }
1099
+ }
1100
+ const baseEnv = opts.baseEnv ?? process.env;
1101
+ const childEnv = { ...baseEnv, ...merged };
1102
+ const cleanup = [];
1103
+ let tmpDir;
1104
+ const cwd = opts.cwd ?? process.cwd();
1105
+ try {
1106
+ const fileResp = await authedFetch(
1107
+ (bearer) => http.envFilesGet(bearer, { project, env })
1108
+ ).catch(() => null);
1109
+ for (const f of fileResp?.files ?? []) {
1110
+ if (f.pointerEnv !== void 0) {
1111
+ tmpDir ??= mkdtempSync(join2(tmpdir(), "env-sync-files-"));
1112
+ const p = join2(tmpDir, basename3(f.name));
1113
+ writeFileSync2(p, f.content, { mode: 384 });
1114
+ childEnv[f.pointerEnv] = p;
1115
+ } else {
1116
+ const p = resolve4(cwd, f.destPath);
1117
+ writeFileSync2(p, f.content, { mode: 384 });
1118
+ cleanup.push(p);
1119
+ }
1120
+ }
1121
+ } catch {
1122
+ }
1123
+ const spawnImpl = opts.spawn ?? defaultSpawn;
1124
+ let exitCode;
1125
+ try {
1126
+ exitCode = await runChild(spawnImpl, opts.command, opts.args ?? [], childEnv);
1127
+ } finally {
1128
+ for (const p of cleanup) rmSync(p, { force: true });
1129
+ if (tmpDir !== void 0) rmSync(tmpDir, { recursive: true, force: true });
1130
+ }
1131
+ return {
1132
+ project,
1133
+ env,
1134
+ command: opts.command,
1135
+ injectedKeys: Object.keys(merged),
1136
+ exitCode
1137
+ };
1138
+ }
1139
+ var defaultSpawn = (command, args, options) => nodeSpawn(command, args, options);
1140
+ function runChild(spawn, command, args, env) {
1141
+ return new Promise((resolveP, rejectP) => {
1142
+ const child = spawn(command, args, { env, stdio: "inherit" });
1143
+ child.on("error", (err) => {
1144
+ rejectP(new EnvSyncError("SPAWN_FAILED", `Failed to run '${command}': ${err.message}`, 127));
1145
+ });
1146
+ child.on("exit", (code) => {
1147
+ resolveP(code ?? 1);
1148
+ });
1149
+ });
1150
+ }
1151
+ async function buildAuthedFetch3(http) {
1152
+ const serviceToken = resolveServiceToken();
1153
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
1154
+ return (call) => call(serviceToken);
1155
+ }
1156
+ const email = await getCurrentUser();
1157
+ if (email === null) throw new AuthRequiredError();
1158
+ const tokens = await loadTokens(email);
1159
+ if (tokens === null) throw new AuthRequiredError();
1160
+ return async (call) => {
1161
+ try {
1162
+ return await call(tokens.idToken);
1163
+ } catch (err) {
1164
+ if (!(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
1165
+ throw err;
1166
+ }
1167
+ const refreshed = await http.refresh({ refreshToken: tokens.refreshToken });
1168
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
1169
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
1170
+ return await call(refreshed.idToken);
1171
+ }
1172
+ };
1173
+ }
1174
+
1175
+ // src/commands/files.ts
1176
+ import { readFile as readFile3 } from "fs/promises";
1177
+ import { basename as basename4, resolve as resolve5 } from "path";
1178
+ async function resolveAuth(opts) {
1179
+ const cfg = await loadProjectConfig(opts.cwd);
1180
+ const project = opts.project ?? cfg.name;
1181
+ const env = resolveEnv(opts.env, process.env.APP_ENV, cfg.envSync.defaultEnv);
1182
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
1183
+ const serviceToken = resolveServiceToken();
1184
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
1185
+ return { project, env, http, bearer: serviceToken };
1186
+ }
1187
+ const email = await getCurrentUser();
1188
+ if (email === null) throw new AuthRequiredError();
1189
+ const tokens = await loadTokens(email);
1190
+ if (tokens === null) throw new AuthRequiredError();
1191
+ return { project, env, http, bearer: tokens.idToken, refreshable: { email, tokens } };
1192
+ }
1193
+ async function withAuth(ctx, call) {
1194
+ try {
1195
+ return await call(ctx.bearer);
1196
+ } catch (err) {
1197
+ if (ctx.refreshable === void 0 || !(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
1198
+ throw err;
1199
+ }
1200
+ const { email, tokens } = ctx.refreshable;
1201
+ const refreshed = await ctx.http.refresh({ refreshToken: tokens.refreshToken });
1202
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
1203
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
1204
+ return await call(refreshed.idToken);
1205
+ }
1206
+ }
1207
+ async function pushCommand(opts) {
1208
+ const cwd = opts.cwd ?? process.cwd();
1209
+ let content;
1210
+ try {
1211
+ content = await readFile3(resolve5(cwd, opts.path), "utf8");
1212
+ } catch (err) {
1213
+ throw new ValidationError(`Could not read ${opts.path}: ${err.message}`);
1214
+ }
1215
+ const name = opts.as ?? (opts.path.startsWith("/") ? basename4(opts.path) : opts.path);
1216
+ const ctx = await resolveAuth(opts);
1217
+ return withAuth(
1218
+ ctx,
1219
+ (bearer) => ctx.http.envFilesPut(
1220
+ bearer,
1221
+ { project: ctx.project, env: ctx.env },
1222
+ { name, content, destPath: opts.as, pointerEnv: opts.pointer }
1223
+ )
1224
+ );
1225
+ }
1226
+ async function filesCommand(opts) {
1227
+ const ctx = await resolveAuth(opts);
1228
+ const res = await withAuth(
1229
+ ctx,
1230
+ (bearer) => ctx.http.envFilesGet(bearer, { project: ctx.project, env: ctx.env })
1231
+ );
1232
+ return res.files.map((f) => ({
1233
+ name: f.name,
1234
+ destPath: f.destPath,
1235
+ pointerEnv: f.pointerEnv,
1236
+ version: 0,
1237
+ valueSha256: "",
1238
+ createdAt: "",
1239
+ createdBy: ""
1240
+ }));
1241
+ }
1242
+
1243
+ // src/scan.ts
1244
+ import { readdirSync, readFileSync, statSync } from "fs";
1245
+ import { join as join3, relative } from "path";
1246
+ var RULES = [
1247
+ { id: "aws-access-key-id", description: "AWS Access Key ID", regex: /AKIA[0-9A-Z]{16}/g },
1248
+ {
1249
+ id: "aws-secret-access-key",
1250
+ description: "Possible AWS Secret Access Key",
1251
+ regex: /aws.{0,20}['"][0-9a-zA-Z/+]{40}['"]/gi
1252
+ },
1253
+ {
1254
+ id: "private-key",
1255
+ description: "Private key block",
1256
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g
1257
+ },
1258
+ { id: "github-pat", description: "GitHub personal access token", regex: /ghp_[0-9A-Za-z]{36}/g },
1259
+ {
1260
+ id: "github-fine-grained-pat",
1261
+ description: "GitHub fine-grained token",
1262
+ regex: /github_pat_[0-9A-Za-z_]{60,}/g
1263
+ },
1264
+ { id: "slack-token", description: "Slack token", regex: /xox[baprs]-[0-9A-Za-z-]{10,}/g },
1265
+ { id: "stripe-secret-key", description: "Stripe secret key", regex: /sk_live_[0-9A-Za-z]{24,}/g },
1266
+ { id: "google-api-key", description: "Google API key", regex: /AIza[0-9A-Za-z_-]{35}/g },
1267
+ {
1268
+ id: "dotvault-service-token",
1269
+ description: "dotvault service token",
1270
+ regex: /est_[0-9a-f]{32}\.[A-Za-z0-9_-]{20,}/g
1271
+ },
1272
+ {
1273
+ id: "generic-assignment",
1274
+ description: "Generic secret assignment",
1275
+ regex: /(?:secret|token|password|passwd|api[_-]?key)["']?\s*[:=]\s*["'][^"'\s]{16,}["']/gi
1276
+ }
1277
+ ];
1278
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1279
+ "node_modules",
1280
+ ".git",
1281
+ "dist",
1282
+ "build",
1283
+ ".next",
1284
+ "coverage",
1285
+ "cdk.out",
1286
+ ".turbo"
1287
+ ]);
1288
+ function redact(match) {
1289
+ if (match.length <= 8) return "*".repeat(match.length);
1290
+ return `${match.slice(0, 4)}...${match.slice(-2)} (${match.length} chars)`;
1291
+ }
1292
+ function scanText(content, file = "<text>") {
1293
+ const findings = [];
1294
+ const lines = content.split(/\r?\n/);
1295
+ for (let i = 0; i < lines.length; i += 1) {
1296
+ const line = lines[i];
1297
+ for (const rule of RULES) {
1298
+ rule.regex.lastIndex = 0;
1299
+ let m;
1300
+ while ((m = rule.regex.exec(line)) !== null) {
1301
+ findings.push({
1302
+ rule: rule.id,
1303
+ description: rule.description,
1304
+ file,
1305
+ line: i + 1,
1306
+ excerpt: redact(m[0])
1307
+ });
1308
+ if (m.index === rule.regex.lastIndex) rule.regex.lastIndex += 1;
1309
+ }
1310
+ }
1311
+ }
1312
+ return findings;
1313
+ }
1314
+ function looksBinary(buf) {
1315
+ const limit = Math.min(buf.length, 8192);
1316
+ for (let i = 0; i < limit; i += 1) {
1317
+ if (buf.charCodeAt(i) === 0) return true;
1318
+ }
1319
+ return false;
1320
+ }
1321
+ var MAX_FILE_BYTES = 5 * 1024 * 1024;
1322
+ function scanPaths(paths, root = process.cwd()) {
1323
+ const findings = [];
1324
+ const visit = (p) => {
1325
+ let st;
1326
+ try {
1327
+ st = statSync(p);
1328
+ } catch {
1329
+ return;
1330
+ }
1331
+ if (st.isDirectory()) {
1332
+ for (const entry of readdirSync(p)) {
1333
+ if (SKIP_DIRS.has(entry)) continue;
1334
+ visit(join3(p, entry));
1335
+ }
1336
+ return;
1337
+ }
1338
+ if (!st.isFile() || st.size > MAX_FILE_BYTES) return;
1339
+ let content;
1340
+ try {
1341
+ content = readFileSync(p, "utf8");
1342
+ } catch {
1343
+ return;
1344
+ }
1345
+ if (looksBinary(content)) return;
1346
+ findings.push(...scanText(content, relative(root, p) || p));
1347
+ };
1348
+ for (const p of paths) visit(p);
1349
+ return findings;
1350
+ }
1351
+
1352
+ // src/commands/write.ts
1353
+ import { readFile as readFile4 } from "fs/promises";
1354
+ import { resolve as resolve6 } from "path";
1355
+ async function resolveContext(opts) {
1356
+ const cfg = await loadProjectConfig(opts.cwd);
1357
+ const project = opts.project ?? cfg.name;
1358
+ const env = resolveEnv(opts.env, process.env.APP_ENV, cfg.envSync.defaultEnv);
1359
+ const fileInput = opts.file ?? cfg.envSync.files[0];
1360
+ const file = slugToFileName(fileNameToSlug(fileInput));
1361
+ const http = opts.http ?? createHttpClient(cfg.proxy.apiUrl);
1362
+ const serviceToken = resolveServiceToken();
1363
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
1364
+ return { project, env, file, http, bearer: serviceToken };
1365
+ }
1366
+ const email = await getCurrentUser();
1367
+ if (email === null) throw new AuthRequiredError();
1368
+ const tokens = await loadTokens(email);
1369
+ if (tokens === null) throw new AuthRequiredError();
1370
+ return { project, env, file, http, bearer: tokens.idToken, refreshable: { email, tokens } };
1371
+ }
1372
+ function parsePairs(pairs) {
1373
+ const out = {};
1374
+ for (const pair of pairs) {
1375
+ const eq = pair.indexOf("=");
1376
+ if (eq <= 0) {
1377
+ throw new ValidationError(`Invalid KEY=VALUE pair: ${JSON.stringify(pair)}`);
1378
+ }
1379
+ out[pair.slice(0, eq)] = pair.slice(eq + 1);
1380
+ }
1381
+ return out;
1382
+ }
1383
+ function parseEnvFile(text) {
1384
+ const out = {};
1385
+ for (const raw of text.split(/\r?\n/)) {
1386
+ const line = raw.trim();
1387
+ if (line.length === 0 || line.startsWith("#")) continue;
1388
+ const body = line.startsWith("export ") ? line.slice(7) : line;
1389
+ const eq = body.indexOf("=");
1390
+ if (eq <= 0) continue;
1391
+ const key = body.slice(0, eq).trim();
1392
+ let val = body.slice(eq + 1).trim();
1393
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
1394
+ val = val.slice(1, -1);
1395
+ }
1396
+ out[key] = val;
1397
+ }
1398
+ return out;
1399
+ }
1400
+ async function setCommand(opts) {
1401
+ const keys = parsePairs(opts.pairs);
1402
+ if (Object.keys(keys).length === 0) {
1403
+ throw new ValidationError("set requires at least one KEY=VALUE");
1404
+ }
1405
+ const ctx = await resolveContext(opts);
1406
+ return fetchWithRefresh2(
1407
+ ctx,
1408
+ (idToken) => ctx.http.envKeysPut(idToken, { project: ctx.project, env: ctx.env, file: ctx.file }, { keys })
1409
+ );
1410
+ }
1411
+ async function deleteCommand(opts) {
1412
+ if (opts.keys.length === 0) throw new ValidationError("delete requires at least one KEY");
1413
+ const ctx = await resolveContext(opts);
1414
+ return fetchWithRefresh2(
1415
+ ctx,
1416
+ (idToken) => ctx.http.envKeysDelete(
1417
+ idToken,
1418
+ { project: ctx.project, env: ctx.env, file: ctx.file },
1419
+ { keys: opts.keys }
1420
+ )
1421
+ );
1422
+ }
1423
+ async function importCommand(opts) {
1424
+ const cwd = opts.cwd ?? process.cwd();
1425
+ let text;
1426
+ try {
1427
+ text = await readFile4(resolve6(cwd, opts.path), "utf8");
1428
+ } catch (err) {
1429
+ throw new ValidationError(`Could not read ${opts.path}: ${err.message}`);
1430
+ }
1431
+ const keys = parseEnvFile(text);
1432
+ if (Object.keys(keys).length === 0) {
1433
+ throw new ValidationError(`No KEY=VALUE lines found in ${opts.path}`);
1434
+ }
1435
+ const ctx = await resolveContext(opts);
1436
+ return fetchWithRefresh2(
1437
+ ctx,
1438
+ (idToken) => ctx.http.envKeysPut(idToken, { project: ctx.project, env: ctx.env, file: ctx.file }, { keys })
1439
+ );
1440
+ }
1441
+ async function describeCommand(opts) {
1442
+ const ctx = await resolveContext(opts);
1443
+ return fetchWithRefresh2(
1444
+ ctx,
1445
+ (idToken) => ctx.http.envKeysPut(
1446
+ idToken,
1447
+ { project: ctx.project, env: ctx.env, file: ctx.file },
1448
+ { descriptions: { [opts.key]: opts.text } }
1449
+ )
1450
+ );
1451
+ }
1452
+ async function versionsCommand(opts) {
1453
+ const ctx = await resolveContext(opts);
1454
+ return fetchWithRefresh2(
1455
+ ctx,
1456
+ (idToken) => ctx.http.envVersions(idToken, {
1457
+ project: ctx.project,
1458
+ env: ctx.env,
1459
+ file: ctx.file,
1460
+ key: opts.key
1461
+ })
1462
+ );
1463
+ }
1464
+ async function rollbackCommand(opts) {
1465
+ if (!Number.isInteger(opts.toVersion) || opts.toVersion < 1) {
1466
+ throw new ValidationError("rollback --to must be a positive integer version");
1467
+ }
1468
+ const ctx = await resolveContext(opts);
1469
+ return fetchWithRefresh2(
1470
+ ctx,
1471
+ (idToken) => ctx.http.envRollback(
1472
+ idToken,
1473
+ { project: ctx.project, env: ctx.env, file: ctx.file },
1474
+ { key: opts.key, toVersion: opts.toVersion }
1475
+ )
1476
+ );
1477
+ }
1478
+ async function fetchWithRefresh2(ctx, call) {
1479
+ try {
1480
+ return await call(ctx.bearer);
1481
+ } catch (err) {
1482
+ if (ctx.refreshable === void 0 || !(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
1483
+ throw err;
1484
+ }
1485
+ const { email, tokens } = ctx.refreshable;
1486
+ const refreshed = await ctx.http.refresh({ refreshToken: tokens.refreshToken });
1487
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
1488
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
1489
+ return await call(refreshed.idToken);
1490
+ }
1491
+ }
1492
+
1493
+ // src/commands/whoami.ts
1494
+ async function whoamiCommand(env = process.env) {
1495
+ const serviceToken = resolveServiceToken(env);
1496
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
1497
+ return {
1498
+ kind: "service",
1499
+ tokenPrefix: serviceToken.slice(0, 12)
1500
+ };
1501
+ }
1502
+ const email = await getCurrentUser();
1503
+ if (email === null) return null;
1504
+ const tokens = await loadTokens(email);
1505
+ if (tokens === null) return null;
1506
+ const now = Math.floor(Date.now() / 1e3);
1507
+ return {
1508
+ kind: "user",
1509
+ email,
1510
+ expiresAt: tokens.expiresAt,
1511
+ expiresInSeconds: Math.max(0, tokens.expiresAt - now)
1512
+ };
1513
+ }
1514
+
1515
+ // src/commands/init.ts
1516
+ import { writeFile as writeFile2, access } from "fs/promises";
1517
+ import { resolve as resolve7, basename as basename5 } from "path";
1518
+ import { constants } from "fs";
1519
+ var CONFIG_FILENAME = "project-config.json";
1520
+ async function initCommand(opts = {}) {
1521
+ const cwd = opts.cwd ?? process.cwd();
1522
+ const path = resolve7(cwd, CONFIG_FILENAME);
1523
+ const project = opts.name ?? basename5(cwd);
1524
+ const apiUrl = opts.api ?? DEFAULT_API_URL;
1525
+ const exists = await fileExists(path);
1526
+ if (exists && !opts.force) {
1527
+ throw new ValidationError(`${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
1528
+ }
1529
+ const config = buildConfig(project, apiUrl);
1530
+ await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
1531
+ return { path, project };
1532
+ }
1533
+ function buildConfig(name, apiUrl) {
1534
+ return {
1535
+ name,
1536
+ setup: {
1537
+ bootstrap: {
1538
+ proxy: {
1539
+ apiUrl
1540
+ }
1541
+ },
1542
+ envSync: {
1543
+ enabled: true,
1544
+ files: [".env"],
1545
+ defaultEnv: "development"
1546
+ }
1547
+ }
1548
+ };
1549
+ }
1550
+ async function fileExists(path) {
1551
+ try {
1552
+ await access(path, constants.F_OK);
1553
+ return true;
1554
+ } catch {
1555
+ return false;
1556
+ }
1557
+ }
1558
+
1559
+ // src/commands/project.ts
1560
+ async function projectCreateCommand(opts) {
1561
+ const cfg = await loadProjectConfig(opts.cwd);
1562
+ const http = createHttpClient(cfg.proxy.apiUrl);
1563
+ const body = {
1564
+ name: opts.name,
1565
+ ...opts.orgId !== void 0 ? { owning_org_id: opts.orgId } : {}
1566
+ };
1567
+ const serviceToken = resolveAdminServiceToken() ?? resolveServiceToken();
1568
+ if (serviceToken !== void 0 && serviceToken.length > 0) {
1569
+ return http.adminProjectsCreate(serviceToken, body);
1570
+ }
1571
+ const authedFetch = await buildAuthedFetch4(http);
1572
+ return authedFetch((bearer) => http.adminProjectsCreate(bearer, body));
1573
+ }
1574
+ async function buildAuthedFetch4(http) {
1575
+ const email = await getCurrentUser();
1576
+ if (email === null) throw new AuthRequiredError();
1577
+ const tokens = await loadTokens(email);
1578
+ if (tokens === null) throw new AuthRequiredError();
1579
+ return async (call) => {
1580
+ try {
1581
+ return await call(tokens.idToken);
1582
+ } catch (err) {
1583
+ if (!(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
1584
+ throw err;
1585
+ }
1586
+ const refreshed = await http.refresh({ refreshToken: tokens.refreshToken });
1587
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
1588
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
1589
+ return await call(refreshed.idToken);
1590
+ }
1591
+ };
1592
+ }
1593
+
1594
+ // src/commands/token.ts
1595
+ async function tokenListCommand(opts) {
1596
+ const cfg = await loadProjectConfig(opts.cwd);
1597
+ const http = createHttpClient(cfg.proxy.apiUrl);
1598
+ const authedFetch = await buildAuthedFetch5(http);
1599
+ return authedFetch((bearer) => http.adminTokensList(bearer, { orgId: opts.orgId }));
1600
+ }
1601
+ async function tokenCreateCommand(opts) {
1602
+ const cfg = await loadProjectConfig(opts.cwd);
1603
+ const http = createHttpClient(cfg.proxy.apiUrl);
1604
+ const authedFetch = await buildAuthedFetch5(http);
1605
+ const body = {
1606
+ name: opts.name,
1607
+ role: opts.role,
1608
+ ...opts.projectId !== void 0 ? { projectId: opts.projectId } : {},
1609
+ ...opts.ttlSeconds !== void 0 ? { ttlSeconds: opts.ttlSeconds } : {}
1610
+ };
1611
+ return authedFetch((bearer) => http.adminTokensCreate(bearer, { orgId: opts.orgId }, body));
1612
+ }
1613
+ async function tokenRevokeCommand(opts) {
1614
+ const cfg = await loadProjectConfig(opts.cwd);
1615
+ const http = createHttpClient(cfg.proxy.apiUrl);
1616
+ const authedFetch = await buildAuthedFetch5(http);
1617
+ await authedFetch(
1618
+ (bearer) => http.adminTokensRevoke(bearer, { orgId: opts.orgId, tokenId: opts.tokenId })
1619
+ );
1620
+ }
1621
+ async function buildAuthedFetch5(http) {
1622
+ const email = await getCurrentUser();
1623
+ if (email === null) throw new AuthRequiredError();
1624
+ const tokens = await loadTokens(email);
1625
+ if (tokens === null) throw new AuthRequiredError();
1626
+ return async (call) => {
1627
+ try {
1628
+ return await call(tokens.idToken);
1629
+ } catch (err) {
1630
+ if (!(err instanceof AuthRequiredError) && !(err instanceof AuthExpiredError)) {
1631
+ throw err;
1632
+ }
1633
+ const refreshed = await http.refresh({ refreshToken: tokens.refreshToken });
1634
+ const expiresAt = Math.floor(Date.now() / 1e3) + refreshed.expiresIn;
1635
+ await saveTokens(email, { ...tokens, idToken: refreshed.idToken, expiresAt });
1636
+ return await call(refreshed.idToken);
1637
+ }
1638
+ };
1639
+ }
1640
+
1641
+ // src/index.ts
1642
+ var useColor = (stream) => stream.isTTY === true && process.env.NO_COLOR === void 0;
1643
+ var paint = (code, s, stream) => useColor(stream) ? `\x1B[${code}m${s}\x1B[0m` : s;
1644
+ var green = (s) => paint("32", s, process.stdout);
1645
+ var red = (s) => paint("31", s, process.stderr);
1646
+ var cyan = (s) => paint("36", s, process.stdout);
1647
+ var bold = (s) => paint("1", s, process.stdout);
1648
+ var dim = (s) => paint("2", s, process.stdout);
1649
+ var PULL_STAGES = ["authenticating", "fetching", "writing"];
1650
+ var PULL_STAGE_LABELS = {
1651
+ authenticating: "Authenticating\u2026",
1652
+ fetching: "Fetching secrets\u2026",
1653
+ writing: "Writing files\u2026"
1654
+ };
1655
+ function renderProgressBar(step, total, width = 20) {
1656
+ const filled = Math.round(step / total * width);
1657
+ return `[${"\u2588".repeat(filled)}${"-".repeat(width - filled)}]`;
1658
+ }
1659
+ function pullProgressReporter() {
1660
+ return (stage) => {
1661
+ if (!useColor(process.stdout)) return;
1662
+ const step = PULL_STAGES.indexOf(stage) + 1;
1663
+ const bar = renderProgressBar(step, PULL_STAGES.length);
1664
+ process.stdout.write(`\r\x1B[K${cyan(bar)} ${dim(PULL_STAGE_LABELS[stage])}`);
1665
+ };
1666
+ }
1667
+ function clearProgressLine() {
1668
+ if (!useColor(process.stdout)) return;
1669
+ process.stdout.write("\r\x1B[K");
1670
+ }
1671
+ function readPackageVersion() {
1672
+ try {
1673
+ const require2 = createRequire(import.meta.url);
1674
+ const pkg = require2("../package.json");
1675
+ return pkg.version;
1676
+ } catch {
1677
+ return "0.0.0-unknown";
1678
+ }
1679
+ }
1680
+ var PACKAGE_VERSION = readPackageVersion();
1681
+ function buildProgram() {
1682
+ const program = new Command();
1683
+ program.enablePositionalOptions();
1684
+ program.name("dotvault").description(
1685
+ "dotvault \u2014 sync .env / .npmrc / .gitconfig and any config file across machines and CI/CD. No AWS account required."
1686
+ ).usage("<command> [options]").version(PACKAGE_VERSION);
1687
+ program.addHelpText(
1688
+ "after",
1689
+ `
1690
+ Examples:
1691
+ $ dotvault login # browser-based auth (Cognito)
1692
+ $ dotvault pull # write .env / .npmrc etc. to local disk
1693
+ $ dotvault run -- npm run dev # inject secrets into child process (no file written)
1694
+ $ dotvault push .npmrc # add any file to the sync set
1695
+ $ dotvault scan . # detect committed secrets
1696
+
1697
+ Common workflows:
1698
+ - First-time setup: dotvault login \u2192 dotvault pull
1699
+ - Local dev: dotvault run -- npm run dev
1700
+ - CI/CD pull: dotvault login (service token) \u2192 dotvault pull --env production
1701
+ - Audit history: dotvault versions DATABASE_URL
1702
+ - Roll back a secret: dotvault rollback DATABASE_URL --to 4
1703
+ - Mint a CI token: dotvault token create --org <id> --name ci --role viewer
1704
+ - Share a key org-wide: dotvault global keys \u2192 dotvault global use <KEY>
1705
+
1706
+ More:
1707
+ Admin console: https://app.dotvault.io
1708
+ Help & FAQ: https://app.dotvault.io/admin/help
1709
+ Docs: https://github.com/EarthLinkNetwork/dotvault#readme`
1710
+ );
1711
+ program.command("init").description("Create project-config.json with sensible defaults").option("-n, --name <name>", "project name (default: current directory name)").option("--api <url>", "API base URL (default: https://api.dotvault.io/v1)").option("-f, --force", "overwrite existing config").addHelpText(
1712
+ "after",
1713
+ `
1714
+ Example:
1715
+ $ dotvault init
1716
+ $ dotvault init --name my-app
1717
+ $ dotvault init --name my-app --api https://self-hosted.example.com/v1`
1718
+ ).action(async (opts) => {
1719
+ const result = await initCommand(opts);
1720
+ process.stdout.write(
1721
+ `Created project-config.json (project=${result.project}). Next: dotvault login && dotvault pull
1722
+ `
1723
+ );
1724
+ });
1725
+ program.command("login").description("Log in with email + password and store tokens in OS keychain").option("-e, --email <email>", "email (skip the prompt)").addHelpText(
1726
+ "after",
1727
+ `
1728
+ Example:
1729
+ $ dotvault login # prompts for email + password
1730
+ $ dotvault login -e you@dotvault.io`
1731
+ ).action(async (opts) => {
1732
+ const result = await loginCommand({ email: opts.email });
1733
+ const expiresIn = Math.max(0, result.expiresAt - Math.floor(Date.now() / 1e3));
1734
+ process.stdout.write(
1735
+ `Logged in as ${result.email}. Token expires in ${expiresIn}s. Refresh token saved.
1736
+ `
1737
+ );
1738
+ });
1739
+ program.command("pull").description("Pull env vars and write them to every declared file (markers preserved).").option("-p, --project <project>", "override project name").option("-E, --env <env>", "environment: development | staging | production").option("-f, --file <file>", "pull only this file (e.g. .env.local)").option("-t, --target <path>", "DEPRECATED \u2014 pre-2.7.0 alias for --file. Use --file instead.").addHelpText(
1740
+ "after",
1741
+ `
1742
+ Example:
1743
+ $ dotvault pull # all declared files
1744
+ $ dotvault pull --file .env.local # .env.local only
1745
+ $ dotvault pull --env staging # staging environment`
1746
+ ).action(async (opts) => {
1747
+ if (opts.target !== void 0 && opts.file === void 0) {
1748
+ process.stderr.write("warning: --target is deprecated as of v0.5.0; use --file instead.\n");
1749
+ }
1750
+ let result;
1751
+ try {
1752
+ result = await pullCommand({ ...opts, onProgress: pullProgressReporter() });
1753
+ } catch (err) {
1754
+ clearProgressLine();
1755
+ throw err;
1756
+ }
1757
+ clearProgressLine();
1758
+ for (const f of result.files) {
1759
+ process.stdout.write(
1760
+ `${green("\u2713")} Wrote ${f.writtenKeys.length} keys to ${f.target} (project=${result.project}, env=${result.env}, file=${f.file}). Markers preserved.
1761
+ `
1762
+ );
1763
+ }
1764
+ const { filesSynced, filesExcluded, totalKeys } = result.summary;
1765
+ process.stdout.write(
1766
+ `${bold(green("\u2714"))} ${filesSynced} file${filesSynced === 1 ? "" : "s"} synced, ${filesExcluded} excluded, ${totalKeys} key${totalKeys === 1 ? "" : "s"} total
1767
+ `
1768
+ );
1769
+ });
1770
+ program.command("run").description(
1771
+ "Inject secrets into a child process (no .env written). Recommended for local dev and CI."
1772
+ ).option("-p, --project <project>", "override project name").option("-E, --env <env>", "environment: development | staging | production").option("-f, --file <file>", "inject only this file (e.g. .env.local)").argument("<command...>", "command to run with secrets injected into its environment").passThroughOptions().addHelpText(
1773
+ "after",
1774
+ `
1775
+ Example:
1776
+ $ dotvault run -- node server.js
1777
+ $ dotvault run --env production -- npm start`
1778
+ ).action(
1779
+ async (commandParts, opts) => {
1780
+ const [command, ...args] = commandParts;
1781
+ const result = await runCommand({
1782
+ project: opts.project,
1783
+ env: opts.env,
1784
+ file: opts.file,
1785
+ command: command ?? "",
1786
+ args
1787
+ });
1788
+ process.exitCode = result.exitCode;
1789
+ }
1790
+ );
1791
+ const writeOpts = (cmd) => cmd.option("-p, --project <project>", "override project name").option("-E, --env <env>", "environment: development | staging | production").option("-f, --file <file>", "target file (e.g. .env.local)");
1792
+ writeOpts(
1793
+ program.command("set").description("Set one or more secrets.").argument("<pairs...>", "KEY=VALUE pairs").addHelpText(
1794
+ "after",
1795
+ `
1796
+ Example:
1797
+ $ dotvault set DATABASE_URL=postgres://localhost
1798
+ $ dotvault set FOO=1 BAR=2 BAZ=3`
1799
+ )
1800
+ ).action(async (pairs, opts) => {
1801
+ const r = await setCommand({ ...opts, pairs });
1802
+ for (const k of r.results) {
1803
+ process.stdout.write(`set ${k.key} (v${k.version}) in ${r.file} [${r.project}/${r.env}]
1804
+ `);
1805
+ }
1806
+ });
1807
+ writeOpts(
1808
+ program.command("describe").description("Set or clear a secret's description (metadata, not the value).").argument("<key>", "the key").argument("<text>", 'description text; pass "" to clear').addHelpText(
1809
+ "after",
1810
+ `
1811
+ Example:
1812
+ $ dotvault describe DATABASE_URL "prod read replica, rotated quarterly"
1813
+ $ dotvault describe DATABASE_URL "" # clears the description`
1814
+ )
1815
+ ).action(
1816
+ async (key, text, opts) => {
1817
+ const r = await describeCommand({ ...opts, key, text });
1818
+ if (r.notFound !== void 0 && r.notFound.length > 0) {
1819
+ process.stderr.write(
1820
+ `warning: ${key} has no current value in ${r.file} [${r.project}/${r.env}] \u2014 description not applied
1821
+ `
1822
+ );
1823
+ process.exitCode = 1;
1824
+ return;
1825
+ }
1826
+ process.stdout.write(`described ${key} in ${r.file} [${r.project}/${r.env}]
1827
+ `);
1828
+ }
1829
+ );
1830
+ writeOpts(
1831
+ program.command("delete").description("Delete one or more secrets.").argument("<keys...>", "keys to delete").addHelpText(
1832
+ "after",
1833
+ `
1834
+ Example:
1835
+ $ dotvault delete OLD_API_KEY`
1836
+ )
1837
+ ).action(async (keys, opts) => {
1838
+ const r = await deleteCommand({ ...opts, keys });
1839
+ process.stdout.write(
1840
+ `deleted ${r.deleted.length} keys from ${r.file} [${r.project}/${r.env}]
1841
+ `
1842
+ );
1843
+ });
1844
+ writeOpts(
1845
+ program.command("import").description("Import all key/value pairs from a .env file.").argument("<path>", "path to a .env file").addHelpText(
1846
+ "after",
1847
+ `
1848
+ Example:
1849
+ $ dotvault import .env.production`
1850
+ )
1851
+ ).action(async (path, opts) => {
1852
+ const r = await importCommand({ ...opts, path });
1853
+ process.stdout.write(
1854
+ `imported ${r.results.length} keys into ${r.file} [${r.project}/${r.env}]
1855
+ `
1856
+ );
1857
+ });
1858
+ writeOpts(
1859
+ program.command("versions").description("Show version history of a secret key.").argument("<key>", "the key").addHelpText(
1860
+ "after",
1861
+ `
1862
+ Example:
1863
+ $ dotvault versions DATABASE_URL`
1864
+ )
1865
+ ).action(async (key, opts) => {
1866
+ const r = await versionsCommand({ ...opts, key });
1867
+ process.stdout.write(
1868
+ `${r.key} [${r.project}/${r.env}/${r.file}] \u2014 ${r.versions.length} versions
1869
+ `
1870
+ );
1871
+ for (const v of r.versions) {
1872
+ process.stdout.write(
1873
+ ` v${v.version} ${v.createdAt} by ${v.createdBy} sha=${v.valueSha256.slice(0, 12)}
1874
+ `
1875
+ );
1876
+ }
1877
+ });
1878
+ writeOpts(
1879
+ program.command("rollback").description("Restore a secret key to a prior version.").argument("<key>", "the key").requiredOption("--to <version>", "version number to restore").addHelpText(
1880
+ "after",
1881
+ `
1882
+ Example:
1883
+ $ dotvault rollback DATABASE_URL --to 4`
1884
+ )
1885
+ ).action(
1886
+ async (key, opts) => {
1887
+ const r = await rollbackCommand({ ...opts, key, toVersion: Number(opts.to) });
1888
+ process.stdout.write(
1889
+ `rolled back ${r.key} to v${r.restoredFrom} (now v${r.version}) in ${r.file} [${r.project}/${r.env}]
1890
+ `
1891
+ );
1892
+ }
1893
+ );
1894
+ program.command("list").description("List the (env, file) inventory available to pull for this project.").option("-p, --project <project>", "override project name").addHelpText(
1895
+ "after",
1896
+ `
1897
+ Example:
1898
+ $ dotvault list
1899
+ $ dotvault list --project my-app`
1900
+ ).action(async (opts) => {
1901
+ const result = await listCommand(opts);
1902
+ if (result.files.length === 0) {
1903
+ process.stdout.write(`project=${result.project}: no files visible.
1904
+ `);
1905
+ return;
1906
+ }
1907
+ const widths = {
1908
+ env: Math.max(3, ...result.files.map((f) => f.env.length)),
1909
+ file: Math.max(4, ...result.files.map((f) => f.file.length)),
1910
+ keys: 4
1911
+ };
1912
+ const pad = (s, w) => s.padEnd(w);
1913
+ process.stdout.write(`project=${result.project}
1914
+ `);
1915
+ process.stdout.write(
1916
+ `${pad("env", widths.env)} ${pad("file", widths.file)} ${pad("keys", widths.keys)} last_modified
1917
+ `
1918
+ );
1919
+ for (const f of result.files) {
1920
+ process.stdout.write(
1921
+ `${pad(f.env, widths.env)} ${pad(f.file, widths.file)} ${pad(String(f.keyCount), widths.keys)} ${f.lastModifiedAt ?? "-"}
1922
+ `
1923
+ );
1924
+ }
1925
+ });
1926
+ const globalGroup = program.command("global").description(`Manage this project's binding to the org's shared "global" key pool.`);
1927
+ globalGroup.command("keys").description("List the org's global key pool (name + description + which envs have a value).").option(
1928
+ "--org <id>",
1929
+ "org id \u2014 required when signed in via `dotvault login` (ignored for service tokens, which already encode their org)"
1930
+ ).addHelpText(
1931
+ "after",
1932
+ `
1933
+ Example:
1934
+ $ dotvault global keys
1935
+ $ dotvault global keys --org org_eln`
1936
+ ).action(async (opts) => {
1937
+ const result = await globalKeysCommand({ orgId: opts.org });
1938
+ if (result.keys.length === 0) {
1939
+ process.stdout.write("org has no global keys.\n");
1940
+ return;
1941
+ }
1942
+ const formatEnvs = (k) => [...k.has_common ? ["common"] : [], ...k.override_envs].join(",") || "-";
1943
+ const widths = {
1944
+ key: Math.max(3, ...result.keys.map((k) => k.key.length)),
1945
+ envs: Math.max(4, ...result.keys.map((k) => formatEnvs(k).length))
1946
+ };
1947
+ const pad = (s, w) => s.padEnd(w);
1948
+ process.stdout.write(`${pad("key", widths.key)} ${pad("envs", widths.envs)} description
1949
+ `);
1950
+ for (const k of result.keys) {
1951
+ process.stdout.write(
1952
+ `${pad(k.key, widths.key)} ${pad(formatEnvs(k), widths.envs)} ${k.description ?? "-"}
1953
+ `
1954
+ );
1955
+ }
1956
+ });
1957
+ globalGroup.command("set").description("Add or update one or more values in the global key pool (ADR-0023).").argument("<pairs...>", "KEY=VALUE pairs").option(
1958
+ "-E, --env <env>",
1959
+ "environment (development/staging/production, or a custom env) \u2014 omit for the common value shared by every env unless overridden"
1960
+ ).option("--org <id>", "org id \u2014 required when signed in via `dotvault login`").addHelpText(
1961
+ "after",
1962
+ `
1963
+ Example:
1964
+ $ dotvault global set SENTRY_DSN=https://... # common value (all envs)
1965
+ $ dotvault global set SENTRY_DSN=https://... -E production # override for production only`
1966
+ ).action(async (pairs, opts) => {
1967
+ const keys = parsePairs(pairs);
1968
+ if (Object.keys(keys).length === 0) {
1969
+ throw new ValidationError("global set requires at least one KEY=VALUE");
1970
+ }
1971
+ const result = await globalSetCommand({ env: opts.env, orgId: opts.org, keys });
1972
+ process.stdout.write(
1973
+ `set ${result.results.length} key(s) in the global pool [${result.env}]
1974
+ `
1975
+ );
1976
+ if (result.notFound !== void 0 && result.notFound.length > 0) {
1977
+ process.stdout.write(`Warning: no description target for: ${result.notFound.join(", ")}
1978
+ `);
1979
+ }
1980
+ });
1981
+ globalGroup.command("delete").description("Remove one or more keys from the global key pool (ADR-0023).").argument("<keys...>", "key names").option(
1982
+ "-E, --env <env>",
1983
+ "environment (development/staging/production, or a custom env) \u2014 omit to delete the common value"
1984
+ ).option("--org <id>", "org id \u2014 required when signed in via `dotvault login`").addHelpText(
1985
+ "after",
1986
+ `
1987
+ Example:
1988
+ $ dotvault global delete SENTRY_DSN # deletes the common value
1989
+ $ dotvault global delete SENTRY_DSN -E production # deletes the production override only`
1990
+ ).action(async (keys, opts) => {
1991
+ const result = await globalDeleteCommand({ env: opts.env, orgId: opts.org, keys });
1992
+ process.stdout.write(
1993
+ `deleted ${result.deleted.length} key(s) from the global pool [${result.env}]
1994
+ `
1995
+ );
1996
+ });
1997
+ globalGroup.command("describe").description("Set or clear (empty text) a global pool key's description (ADR-0020).").argument("<KEY>", "global key name").argument("<text>", "description text (empty string clears it)").option(
1998
+ "-E, --env <env>",
1999
+ "environment (development/staging/production, or a custom env) \u2014 omit for the common value"
2000
+ ).option("--org <id>", "org id \u2014 required when signed in via `dotvault login`").addHelpText(
2001
+ "after",
2002
+ `
2003
+ Example:
2004
+ $ dotvault global describe SENTRY_DSN "Shared Sentry project DSN" -E production`
2005
+ ).action(async (key, text, opts) => {
2006
+ const result = await globalDescribeCommand({ env: opts.env, orgId: opts.org, key, text });
2007
+ process.stdout.write(`described ${key} in the global pool [${result.env}]
2008
+ `);
2009
+ });
2010
+ globalGroup.command("status").description("Show this project's current global-key bindings.").option("-p, --project <project>", "override project name").addHelpText(
2011
+ "after",
2012
+ `
2013
+ Example:
2014
+ $ dotvault global status`
2015
+ ).action(async (opts) => {
2016
+ const result = await globalStatusCommand(opts);
2017
+ if (result.bindings.length === 0) {
2018
+ process.stdout.write(`project=${result.project}: no global keys bound.
2019
+ `);
2020
+ return;
2021
+ }
2022
+ process.stdout.write(`project=${result.project}
2023
+ `);
2024
+ for (const b of result.bindings) {
2025
+ process.stdout.write(b.alias !== void 0 ? `${b.key} -> ${b.alias}
2026
+ ` : `${b.key}
2027
+ `);
2028
+ }
2029
+ });
2030
+ globalGroup.command("use").description("Bind an org global key to this project (received on the next pull).").argument("<KEY>", "global key name").option("-p, --project <project>", "override project name").option("--as <name>", "rename the key on output (defaults to the key name)").addHelpText(
2031
+ "after",
2032
+ `
2033
+ Example:
2034
+ $ dotvault global use SENTRY_DSN
2035
+ $ dotvault global use GH_TOKEN --as GITHUB_TOKEN`
2036
+ ).action(async (key, opts) => {
2037
+ const result = await globalUseCommand({ project: opts.project, key, alias: opts.as });
2038
+ const asSuffix = opts.as !== void 0 ? ` (as ${opts.as})` : "";
2039
+ process.stdout.write(`Bound ${key}${asSuffix} to ${result.project}.
2040
+ `);
2041
+ if (result.unknown_keys !== void 0 && result.unknown_keys.length > 0) {
2042
+ process.stdout.write(
2043
+ `Warning: the global project has no value yet for: ${result.unknown_keys.join(", ")}
2044
+ `
2045
+ );
2046
+ }
2047
+ });
2048
+ globalGroup.command("drop").description("Unbind an org global key from this project.").argument("<KEY>", "global key name").option("-p, --project <project>", "override project name").addHelpText(
2049
+ "after",
2050
+ `
2051
+ Example:
2052
+ $ dotvault global drop SENTRY_DSN`
2053
+ ).action(async (key, opts) => {
2054
+ const result = await globalDropCommand({ project: opts.project, key });
2055
+ process.stdout.write(`Dropped ${key} from ${result.project}.
2056
+ `);
2057
+ });
2058
+ writeOpts(
2059
+ program.command("push").description("Sync a whole file as-is (.npmrc, kubeconfig, certs, etc.). Zero extra config.").argument("<path>", "file to upload").option("--as <destPath>", "destination path on pull/run (default: same as path)").option("--pointer <env>", "pointer env var for run (default: known-tool table)").addHelpText(
2060
+ "after",
2061
+ `
2062
+ Example:
2063
+ $ dotvault push .npmrc
2064
+ $ dotvault push ~/.kube/config --as .kube/config --pointer KUBECONFIG`
2065
+ )
2066
+ ).action(
2067
+ async (path, opts) => {
2068
+ const r = await pushCommand({
2069
+ project: opts.project,
2070
+ env: opts.env,
2071
+ path,
2072
+ as: opts.as,
2073
+ pointer: opts.pointer
2074
+ });
2075
+ process.stdout.write(
2076
+ `pushed ${r.name} (v${r.version}) \u2192 ${r.destPath}` + (r.pointerEnv ? ` [run sets ${r.pointerEnv}]` : "") + "\n"
2077
+ );
2078
+ }
2079
+ );
2080
+ program.command("files").description("List files synced for this project/env.").option("-p, --project <project>", "override project name").option("-E, --env <env>", "environment").addHelpText(
2081
+ "after",
2082
+ `
2083
+ Example:
2084
+ $ dotvault files
2085
+ $ dotvault files --env production`
2086
+ ).action(async (opts) => {
2087
+ const files = await filesCommand(opts);
2088
+ if (files.length === 0) {
2089
+ process.stdout.write("No files synced.\n");
2090
+ return;
2091
+ }
2092
+ for (const f of files) {
2093
+ process.stdout.write(
2094
+ `${f.name} \u2192 ${f.destPath}${f.pointerEnv ? ` (${f.pointerEnv})` : ""}
2095
+ `
2096
+ );
2097
+ }
2098
+ });
2099
+ program.command("scan").description("Scan files for committed secrets (offline). Exits non-zero if any found.").argument("[paths...]", "files or directories to scan (default: current directory)").addHelpText(
2100
+ "after",
2101
+ `
2102
+ Example:
2103
+ $ dotvault scan . # current directory
2104
+ $ dotvault scan src/ tests/ # multiple targets`
2105
+ ).action((paths) => {
2106
+ const targets = paths.length > 0 ? paths : ["."];
2107
+ const findings = scanPaths(targets);
2108
+ if (findings.length === 0) {
2109
+ process.stdout.write("No secrets found.\n");
2110
+ return;
2111
+ }
2112
+ for (const f of findings) {
2113
+ process.stdout.write(`${f.file}:${f.line} [${f.rule}] ${f.description} \u2014 ${f.excerpt}
2114
+ `);
2115
+ }
2116
+ process.stderr.write(`
2117
+ ${findings.length} potential secret(s) found.
2118
+ `);
2119
+ process.exitCode = 1;
2120
+ });
2121
+ const token = program.command("token").description("Manage service tokens (CI/CD machine identity). Requires org_admin.");
2122
+ token.command("list").description("List service tokens for an org.").requiredOption("--org <id>", "org id (see `dotvault whoami` or the admin console)").addHelpText(
2123
+ "after",
2124
+ `
2125
+ Example:
2126
+ $ dotvault token list --org org_abc123`
2127
+ ).action(async (opts) => {
2128
+ const result = await tokenListCommand({ orgId: opts.org });
2129
+ if (result.tokens.length === 0) {
2130
+ process.stdout.write("No service tokens.\n");
2131
+ return;
2132
+ }
2133
+ const widths = {
2134
+ id: Math.max(8, ...result.tokens.map((t) => t.tokenId.length)),
2135
+ name: Math.max(4, ...result.tokens.map((t) => t.name.length)),
2136
+ role: 6
2137
+ };
2138
+ const pad = (s, w) => s.padEnd(w);
2139
+ process.stdout.write(
2140
+ `${pad("token_id", widths.id)} ${pad("name", widths.name)} ${pad("role", widths.role)} expires_at revoked
2141
+ `
2142
+ );
2143
+ for (const t of result.tokens) {
2144
+ const exp = t.expiresAt === null ? "never" : new Date(t.expiresAt * 1e3).toISOString();
2145
+ process.stdout.write(
2146
+ `${pad(t.tokenId, widths.id)} ${pad(t.name, widths.name)} ${pad(t.role, widths.role)} ${exp} ${t.revoked ? "yes" : "no"}
2147
+ `
2148
+ );
2149
+ }
2150
+ });
2151
+ token.command("create").description("Mint a new service token. The token value is shown ONCE.").requiredOption("--org <id>", "org id").requiredOption("--name <name>", 'human-readable label (e.g. "ci-prod")').requiredOption("--role <role>", "viewer | member | provisioner").option("--project <id>", "restrict the token to a single project").option("--ttl <seconds>", "seconds until expiry (default: never)", (v) => Number(v)).addHelpText(
2152
+ "after",
2153
+ `
2154
+ Example:
2155
+ $ dotvault token create --org org_abc --name ci-prod --role viewer
2156
+ $ dotvault token create --org org_abc --name ci-deploy --role member --ttl 2592000
2157
+ $ dotvault token create --org org_abc --name claude-provision --role provisioner
2158
+
2159
+ A provisioner token (ADR-0018) can create projects but cannot read or write
2160
+ secrets; use it as DOTVAULT_ADMIN_TOKEN (or legacy ENV_SYNC_ADMIN_TOKEN) for
2161
+ headless \`project create\`.
2162
+
2163
+ Save the printed est_\u2026 token in your CI secret store immediately \u2014 it cannot
2164
+ be retrieved again.`
2165
+ ).action(
2166
+ async (opts) => {
2167
+ if (opts.role !== "viewer" && opts.role !== "member" && opts.role !== "provisioner") {
2168
+ process.stderr.write(
2169
+ `role must be 'viewer', 'member', or 'provisioner' (got '${opts.role}')
2170
+ `
2171
+ );
2172
+ process.exitCode = 1;
2173
+ return;
2174
+ }
2175
+ if (opts.ttl !== void 0 && (!Number.isFinite(opts.ttl) || opts.ttl <= 0)) {
2176
+ process.stderr.write(`--ttl must be a positive integer (seconds)
2177
+ `);
2178
+ process.exitCode = 1;
2179
+ return;
2180
+ }
2181
+ const result = await tokenCreateCommand({
2182
+ orgId: opts.org,
2183
+ name: opts.name,
2184
+ role: opts.role,
2185
+ projectId: opts.project,
2186
+ ttlSeconds: opts.ttl
2187
+ });
2188
+ process.stdout.write(`token_id: ${result.tokenId}
2189
+ `);
2190
+ process.stdout.write(`name: ${result.name}
2191
+ `);
2192
+ process.stdout.write(`role: ${result.role}
2193
+ `);
2194
+ if (result.projectId !== void 0) {
2195
+ process.stdout.write(`project: ${result.projectId}
2196
+ `);
2197
+ }
2198
+ process.stdout.write(
2199
+ `expires: ${result.expiresAt === null ? "never" : new Date(result.expiresAt * 1e3).toISOString()}
2200
+ `
2201
+ );
2202
+ process.stdout.write(`
2203
+ token (save now \u2014 shown only once):
2204
+ ${result.token}
2205
+ `);
2206
+ }
2207
+ );
2208
+ token.command("revoke").description("Revoke a service token by id.").requiredOption("--org <id>", "org id").requiredOption("--id <token_id>", "token id to revoke").addHelpText(
2209
+ "after",
2210
+ `
2211
+ Example:
2212
+ $ dotvault token revoke --org org_abc --id est_meta_xyz`
2213
+ ).action(async (opts) => {
2214
+ await tokenRevokeCommand({ orgId: opts.org, tokenId: opts.id });
2215
+ process.stdout.write(`Revoked token ${opts.id}.
2216
+ `);
2217
+ });
2218
+ const project = program.command("project").description("Manage projects (create without the admin web console \u2014 ADR-0018).");
2219
+ project.command("create").description("Create a project. Works headlessly with a provisioner token.").requiredOption("--name <name>", "project name (^[a-z0-9][a-z0-9_-]*$)").option("--org <id>", "owning org id (optional with a provisioner token)").addHelpText(
2220
+ "after",
2221
+ `
2222
+ Credential precedence: DOTVAULT_ADMIN_TOKEN (or legacy ENV_SYNC_ADMIN_TOKEN) \u2192
2223
+ DOTVAULT_TOKEN (or legacy ENV_SYNC_TOKEN) \u2192 keychain login.
2224
+
2225
+ Example (headless \u2014 Claude Code / CI):
2226
+ $ DOTVAULT_ADMIN_TOKEN=est_\u2026 dotvault project create --name myapp
2227
+
2228
+ Example (interactive):
2229
+ $ dotvault login && dotvault project create --name myapp --org org_abc`
2230
+ ).action(async (opts) => {
2231
+ const result = await projectCreateCommand({ name: opts.name, orgId: opts.org });
2232
+ process.stdout.write(`project: ${result.project}
2233
+ `);
2234
+ if (result.project_id !== void 0) {
2235
+ process.stdout.write(`project_id: ${result.project_id}
2236
+ `);
2237
+ }
2238
+ if (result.owning_org_id !== void 0) {
2239
+ process.stdout.write(`org: ${result.owning_org_id}
2240
+ `);
2241
+ }
2242
+ process.stdout.write(`created_at: ${result.createdAt}
2243
+ `);
2244
+ });
2245
+ program.command("logout").description("Remove stored tokens from the OS keychain.").action(async () => {
2246
+ const email = await logoutCommand();
2247
+ if (email === null) {
2248
+ process.stdout.write("Not logged in.\n");
2249
+ } else {
2250
+ process.stdout.write(`Logged out ${email}.
2251
+ `);
2252
+ }
2253
+ });
2254
+ program.command("whoami").description("Show the logged-in user and token expiry.").addHelpText(
2255
+ "after",
2256
+ `
2257
+ Example:
2258
+ $ dotvault whoami`
2259
+ ).action(async () => {
2260
+ const result = await whoamiCommand();
2261
+ if (result === null) {
2262
+ process.stdout.write("Not logged in.\n");
2263
+ process.exitCode = 3;
2264
+ return;
2265
+ }
2266
+ if (result.kind === "service") {
2267
+ process.stdout.write(
2268
+ `Service token (DOTVAULT_TOKEN=${result.tokenPrefix}\u2026) \u2014 CI / Lambda mode
2269
+ `
2270
+ );
2271
+ return;
2272
+ }
2273
+ process.stdout.write(`${result.email} (token expires in ${result.expiresInSeconds}s)
2274
+ `);
2275
+ });
2276
+ return program;
2277
+ }
2278
+ function renameNoticeFor(argv1) {
2279
+ if (argv1 === void 0) return null;
2280
+ const lastSegment = argv1.split(/[/\\]/).pop() ?? argv1;
2281
+ const base = basename6(lastSegment).replace(/\.(cmd|ps1|exe)$/i, "");
2282
+ if (base !== "env-sync") return null;
2283
+ return "env-sync is now dotvault \u2014 the env-sync command keeps working as an alias.";
2284
+ }
2285
+ async function main(argv, argv1 = process.argv[1]) {
2286
+ const notice = renameNoticeFor(argv1);
2287
+ if (notice !== null) {
2288
+ process.stderr.write(`${notice}
2289
+ `);
2290
+ }
2291
+ const program = buildProgram();
2292
+ try {
2293
+ await program.parseAsync(argv, { from: "user" });
2294
+ const code = process.exitCode;
2295
+ return typeof code === "number" ? code : 0;
2296
+ } catch (err) {
2297
+ if (isEnvSyncError(err)) {
2298
+ process.stderr.write(`${red("\u2716")} ${err.code}: ${err.message}
2299
+ `);
2300
+ return err.exitCode;
2301
+ }
2302
+ process.stderr.write(`${red("\u2716")} unexpected: ${err.message}
2303
+ `);
2304
+ return 99;
2305
+ }
2306
+ }
2307
+ var isDirectRun = (() => {
2308
+ if (process.argv[1] === void 0) return false;
2309
+ try {
2310
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
2311
+ } catch {
2312
+ return false;
2313
+ }
2314
+ })();
2315
+ if (isDirectRun) {
2316
+ main(process.argv.slice(2)).then((code) => {
2317
+ process.exit(code);
2318
+ });
2319
+ }
2320
+ export {
2321
+ PACKAGE_VERSION,
2322
+ buildProgram,
2323
+ main,
2324
+ renameNoticeFor
2325
+ };
2326
+ //# sourceMappingURL=index.js.map