xtra-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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -0
  3. package/dist/bin/xtra.js +124 -0
  4. package/dist/commands/access.js +107 -0
  5. package/dist/commands/admin.js +118 -0
  6. package/dist/commands/audit.js +67 -0
  7. package/dist/commands/branch.js +216 -0
  8. package/dist/commands/checkout.js +74 -0
  9. package/dist/commands/ci.js +330 -0
  10. package/dist/commands/completion.js +227 -0
  11. package/dist/commands/diff.js +163 -0
  12. package/dist/commands/doctor.js +176 -0
  13. package/dist/commands/env.js +70 -0
  14. package/dist/commands/export.js +84 -0
  15. package/dist/commands/generate.js +180 -0
  16. package/dist/commands/history.js +77 -0
  17. package/dist/commands/import.js +122 -0
  18. package/dist/commands/init.js +162 -0
  19. package/dist/commands/integration.js +188 -0
  20. package/dist/commands/local.js +198 -0
  21. package/dist/commands/login.js +176 -0
  22. package/dist/commands/login.test.js +51 -0
  23. package/dist/commands/logs.js +121 -0
  24. package/dist/commands/profile.js +184 -0
  25. package/dist/commands/project.js +98 -0
  26. package/dist/commands/rollback.js +96 -0
  27. package/dist/commands/rotate.js +94 -0
  28. package/dist/commands/run.js +215 -0
  29. package/dist/commands/scan.js +127 -0
  30. package/dist/commands/secrets.js +265 -0
  31. package/dist/commands/simulate.js +92 -0
  32. package/dist/commands/status.js +94 -0
  33. package/dist/commands/template.js +276 -0
  34. package/dist/commands/ui.js +218 -0
  35. package/dist/commands/watch.js +121 -0
  36. package/dist/lib/api.js +172 -0
  37. package/dist/lib/api.test.js +89 -0
  38. package/dist/lib/audit.js +136 -0
  39. package/dist/lib/config.js +42 -0
  40. package/dist/lib/config.test.js +47 -0
  41. package/dist/lib/crypto.js +50 -0
  42. package/dist/lib/manifest.js +52 -0
  43. package/dist/lib/profiles.js +103 -0
  44. package/package.json +67 -0
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logsCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const audit_1 = require("../lib/audit");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const table_1 = require("table");
11
+ exports.logsCommand = new commander_1.Command("logs")
12
+ .description("View local audit logs")
13
+ .option("-n, --limit <number>", "Number of logs to show", "20")
14
+ .option("--sync", "Sync logs to cloud", false)
15
+ .option("--event <type>", "Filter by event type (e.g. SECRET_UPDATE, SECRET_ROTATE)")
16
+ .option("--project <projectId>", "Filter by project ID")
17
+ .option("--since <duration>", "Show logs since (e.g. 1h, 24h, 7d, 30d)", "")
18
+ .option("--json", "Output raw JSON instead of table", false)
19
+ .action(async (options) => {
20
+ const { limit, sync, event: eventFilter, project: projectFilter, since, json: jsonOut } = options;
21
+ try {
22
+ let logs = (0, audit_1.loadAuditLogs)();
23
+ if (sync) {
24
+ const unsynced = logs.filter(l => !l.synced);
25
+ if (unsynced.length === 0) {
26
+ console.log(chalk_1.default.green("All logs are already synced."));
27
+ return;
28
+ }
29
+ const spinner = require("ora")(`Syncing ${unsynced.length} logs to cloud...`).start();
30
+ try {
31
+ const { api } = require("../lib/api");
32
+ const { saveAuditLogs } = require("../lib/audit");
33
+ await api.syncLogs(unsynced);
34
+ unsynced.forEach(l => l.synced = true);
35
+ saveAuditLogs(logs);
36
+ spinner.succeed(`Successfully synced ${unsynced.length} logs.`);
37
+ }
38
+ catch (err) {
39
+ spinner.fail("Sync failed.");
40
+ console.error(chalk_1.default.red(err.message));
41
+ }
42
+ return;
43
+ }
44
+ if (logs.length === 0) {
45
+ console.log(chalk_1.default.yellow("No local audit logs found."));
46
+ return;
47
+ }
48
+ // ── Filtering ─────────────────────────────────────────────────────────
49
+ // Filter by event type
50
+ if (eventFilter) {
51
+ const upper = eventFilter.toUpperCase();
52
+ logs = logs.filter(l => l.action?.toUpperCase().includes(upper));
53
+ }
54
+ // Filter by project
55
+ if (projectFilter) {
56
+ logs = logs.filter(l => l.projectId === projectFilter);
57
+ }
58
+ // Filter by --since duration (1h, 6h, 24h, 7d, 30d)
59
+ if (since) {
60
+ const match = since.match(/^(\d+)(h|d|m)$/i);
61
+ if (!match) {
62
+ console.error(chalk_1.default.red(`Invalid --since format: '${since}'. Use e.g. 1h, 24h, 7d, 30d`));
63
+ process.exit(1);
64
+ }
65
+ const amount = parseInt(match[1]);
66
+ const unit = match[2].toLowerCase();
67
+ const msMap = { h: 3600000, d: 86400000, m: 60000 };
68
+ const cutoff = new Date(Date.now() - amount * msMap[unit]);
69
+ logs = logs.filter(l => new Date(l.timestamp) >= cutoff);
70
+ }
71
+ // Sort newest first
72
+ logs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
73
+ const limitedLogs = logs.slice(0, parseInt(limit));
74
+ if (limitedLogs.length === 0) {
75
+ console.log(chalk_1.default.yellow("No logs matched your filters."));
76
+ return;
77
+ }
78
+ // ── Output ────────────────────────────────────────────────────────────
79
+ if (jsonOut) {
80
+ process.stdout.write(JSON.stringify(limitedLogs, null, 2) + "\n");
81
+ return;
82
+ }
83
+ const data = [
84
+ [chalk_1.default.bold("Timestamp"), chalk_1.default.bold("Action"), chalk_1.default.bold("Project"), chalk_1.default.bold("Sync"), chalk_1.default.bold("Details")]
85
+ ];
86
+ limitedLogs.forEach(l => {
87
+ let details = "";
88
+ if (l.details) {
89
+ if (l.details.keys)
90
+ details = `Keys: ${l.details.keys.join(", ")}`;
91
+ else if (l.details.key)
92
+ details = `Key: ${l.details.key}`;
93
+ else
94
+ details = JSON.stringify(l.details);
95
+ }
96
+ const proj = l.projectId ? l.projectId.substring(0, 8) + "..." : "-";
97
+ data.push([
98
+ new Date(l.timestamp).toLocaleString(),
99
+ l.action,
100
+ proj,
101
+ l.synced ? chalk_1.default.green("✔") : chalk_1.default.yellow("✖"),
102
+ details.substring(0, 50) + (details.length > 50 ? "..." : "")
103
+ ]);
104
+ });
105
+ // Show active filters in header
106
+ const filterParts = [];
107
+ if (eventFilter)
108
+ filterParts.push(`event: ${eventFilter.toUpperCase()}`);
109
+ if (projectFilter)
110
+ filterParts.push(`project: ${projectFilter}`);
111
+ if (since)
112
+ filterParts.push(`since: ${since}`);
113
+ const filterHint = filterParts.length > 0 ? chalk_1.default.gray(` [${filterParts.join(" | ")}]`) : "";
114
+ console.log(chalk_1.default.bold(`\nLocal Audit Logs${filterHint}`));
115
+ console.log((0, table_1.table)(data));
116
+ console.log(chalk_1.default.gray(` Showing ${limitedLogs.length} of ${logs.length} total logs.`));
117
+ }
118
+ catch (error) {
119
+ console.error(chalk_1.default.red("Failed to load logs: " + error.message));
120
+ }
121
+ });
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.profileCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const table_1 = require("table");
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const profiles_1 = require("../lib/profiles");
12
+ // ─── Helper ───────────────────────────────────────────────────────────────────
13
+ const safeError = (e) => e?.message || "Unknown error";
14
+ // ─── xtra profile ─────────────────────────────────────────────────────────────
15
+ exports.profileCommand = new commander_1.Command("profile")
16
+ .description("Manage named configuration profiles (token, apiUrl, project per profile)")
17
+ .addHelpText("after", `
18
+ Examples:
19
+ $ xtra profile list # Show all profiles
20
+ $ xtra profile create work --url https://app.xtrasecurity.io/api
21
+ $ xtra profile use work # Switch active profile
22
+ $ xtra profile set work --project abc123 # Set a value in a profile
23
+ $ xtra profile delete old-profile # Remove a profile
24
+ $ xtra --profile work secrets list # Use profile for one command
25
+ `);
26
+ // ── LIST ──────────────────────────────────────────────────────────────────────
27
+ exports.profileCommand
28
+ .command("list")
29
+ .alias("ls")
30
+ .description("List all saved profiles")
31
+ .action(() => {
32
+ const profiles = (0, profiles_1.listProfiles)();
33
+ const active = (0, profiles_1.getActiveProfileName)();
34
+ if (Object.keys(profiles).length === 0) {
35
+ console.log(chalk_1.default.yellow("No profiles found."));
36
+ return;
37
+ }
38
+ const rows = [
39
+ [chalk_1.default.bold("Profile"), chalk_1.default.bold("API URL"), chalk_1.default.bold("Project"), chalk_1.default.bold("Status")]
40
+ ];
41
+ for (const [name, data] of Object.entries(profiles)) {
42
+ const isActive = name === active;
43
+ rows.push([
44
+ isActive ? chalk_1.default.green(`▶ ${name}`) : chalk_1.default.white(` ${name}`),
45
+ chalk_1.default.gray(data.apiUrl || "(default)"),
46
+ chalk_1.default.cyan(data.project || "(not set)"),
47
+ isActive ? chalk_1.default.green("active") : chalk_1.default.gray("inactive"),
48
+ ]);
49
+ }
50
+ console.log(chalk_1.default.bold("\nProfiles:\n"));
51
+ console.log((0, table_1.table)(rows));
52
+ console.log(chalk_1.default.gray(`Tip: Use XTRA_PROFILE=<name> to override per-command.\n`));
53
+ });
54
+ // ── CREATE ────────────────────────────────────────────────────────────────────
55
+ exports.profileCommand
56
+ .command("create <name>")
57
+ .description("Create a new profile")
58
+ .option("--url <apiUrl>", "XtraSecurity API URL for this profile")
59
+ .option("--project <projectId>", "Default project ID for this profile")
60
+ .option("--token <token>", "Auth token for this profile")
61
+ .action(async (name, options) => {
62
+ try {
63
+ // If no options given, prompt interactively
64
+ let { url, project, token } = options;
65
+ if (!url && !project && !token) {
66
+ const answers = await inquirer_1.default.prompt([
67
+ {
68
+ type: "input",
69
+ name: "url",
70
+ message: "API URL:",
71
+ default: "http://localhost:3000/api",
72
+ },
73
+ {
74
+ type: "input",
75
+ name: "project",
76
+ message: "Default Project ID (optional):",
77
+ default: "",
78
+ },
79
+ ]);
80
+ url = answers.url;
81
+ project = answers.project || undefined;
82
+ }
83
+ (0, profiles_1.createProfile)(name, { apiUrl: url, project: project || undefined, token: token || undefined });
84
+ console.log(chalk_1.default.green(`✅ Profile '${name}' created!`));
85
+ console.log(chalk_1.default.gray(` Activate it with: xtra profile use ${name}`));
86
+ }
87
+ catch (e) {
88
+ console.error(chalk_1.default.red(safeError(e)));
89
+ }
90
+ });
91
+ // ── USE ───────────────────────────────────────────────────────────────────────
92
+ exports.profileCommand
93
+ .command("use <name>")
94
+ .description("Switch to a different profile (persists across sessions)")
95
+ .action((name) => {
96
+ try {
97
+ (0, profiles_1.setActiveProfile)(name);
98
+ console.log(chalk_1.default.green(`✅ Switched to profile '${name}'.`));
99
+ console.log(chalk_1.default.gray(` All future commands will use this profile.`));
100
+ }
101
+ catch (e) {
102
+ console.error(chalk_1.default.red(safeError(e)));
103
+ }
104
+ });
105
+ // ── SET ───────────────────────────────────────────────────────────────────────
106
+ exports.profileCommand
107
+ .command("set <name>")
108
+ .description("Update a value in an existing profile")
109
+ .option("--url <apiUrl>", "Update the API URL")
110
+ .option("--project <projectId>", "Update the default project ID")
111
+ .option("--token <token>", "Update the auth token")
112
+ .option("--env <environment>", "Update the default environment")
113
+ .action((name, options) => {
114
+ const fieldMap = {
115
+ url: "apiUrl",
116
+ project: "project",
117
+ token: "token",
118
+ env: "env",
119
+ };
120
+ const updated = [];
121
+ try {
122
+ for (const [flag, field] of Object.entries(fieldMap)) {
123
+ if (options[flag]) {
124
+ (0, profiles_1.setProfileValue)(name, field, options[flag]);
125
+ updated.push(`${field} = ${field === "token" ? "****" : options[flag]}`);
126
+ }
127
+ }
128
+ if (updated.length === 0) {
129
+ console.log(chalk_1.default.yellow("No values provided. Use --url, --project, --token, or --env."));
130
+ return;
131
+ }
132
+ console.log(chalk_1.default.green(`✅ Profile '${name}' updated:`));
133
+ updated.forEach(u => console.log(chalk_1.default.gray(` ${u}`)));
134
+ }
135
+ catch (e) {
136
+ console.error(chalk_1.default.red(safeError(e)));
137
+ }
138
+ });
139
+ // ── CURRENT ───────────────────────────────────────────────────────────────────
140
+ exports.profileCommand
141
+ .command("current")
142
+ .description("Show the currently active profile")
143
+ .action(() => {
144
+ const name = (0, profiles_1.getActiveProfileName)();
145
+ const profiles = (0, profiles_1.listProfiles)();
146
+ const data = profiles[name] || {};
147
+ console.log(chalk_1.default.bold(`\nActive Profile: ${chalk_1.default.green(name)}\n`));
148
+ const rows = [
149
+ [chalk_1.default.bold("Key"), chalk_1.default.bold("Value")],
150
+ ["API URL", chalk_1.default.cyan(data.apiUrl || "(default)")],
151
+ ["Project", chalk_1.default.cyan(data.project || "(not set)")],
152
+ ["Branch", chalk_1.default.cyan(data.branch || "(not set)")],
153
+ ["Env", chalk_1.default.cyan(data.env || "(not set)")],
154
+ ["Token", data.token ? chalk_1.default.green("Set ✓") : chalk_1.default.gray("Not set")],
155
+ ];
156
+ console.log((0, table_1.table)(rows));
157
+ });
158
+ // ── DELETE ────────────────────────────────────────────────────────────────────
159
+ exports.profileCommand
160
+ .command("delete <name>")
161
+ .alias("rm")
162
+ .description("Delete a profile")
163
+ .option("-y, --yes", "Skip confirmation prompt")
164
+ .action(async (name, options) => {
165
+ try {
166
+ if (!options.yes) {
167
+ const { confirm } = await inquirer_1.default.prompt([{
168
+ type: "confirm",
169
+ name: "confirm",
170
+ message: chalk_1.default.red(`Delete profile '${name}'? This cannot be undone.`),
171
+ default: false,
172
+ }]);
173
+ if (!confirm) {
174
+ console.log(chalk_1.default.gray("Cancelled."));
175
+ return;
176
+ }
177
+ }
178
+ (0, profiles_1.deleteProfile)(name);
179
+ console.log(chalk_1.default.green(`✅ Profile '${name}' deleted.`));
180
+ }
181
+ catch (e) {
182
+ console.error(chalk_1.default.red(safeError(e)));
183
+ }
184
+ });
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.projectCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const ora_1 = __importDefault(require("ora"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const config_1 = require("../lib/config");
12
+ const api_1 = require("../lib/api");
13
+ exports.projectCommand = new commander_1.Command("project")
14
+ .description("Manage project context");
15
+ // SET
16
+ exports.projectCommand
17
+ .command("set")
18
+ .description("Set the default project ID")
19
+ .argument("[projectId]", "Project ID to set as default (optional - will show list if not provided)")
20
+ .action(async (projectId) => {
21
+ // If projectId is provided, set it directly
22
+ if (projectId) {
23
+ (0, config_1.setConfig)("project", projectId);
24
+ console.log(chalk_1.default.green(`✔ Default project set to '${projectId}'`));
25
+ const currentBranch = (0, config_1.getConfigValue)("branch");
26
+ if (currentBranch) {
27
+ console.log(chalk_1.default.gray(` Active branch: ${currentBranch}`));
28
+ }
29
+ return;
30
+ }
31
+ // Otherwise, fetch projects and show interactive selector
32
+ const spinner = (0, ora_1.default)("Fetching your projects...").start();
33
+ try {
34
+ const projects = await api_1.api.getProjects();
35
+ spinner.stop();
36
+ if (!Array.isArray(projects) || projects.length === 0) {
37
+ console.log(chalk_1.default.yellow("No projects found."));
38
+ return;
39
+ }
40
+ const choices = projects.map((p) => ({
41
+ name: `${p.name} (${p.id})`,
42
+ value: p.id,
43
+ short: p.name
44
+ }));
45
+ const { selectedProject } = await inquirer_1.default.prompt([{
46
+ type: "list",
47
+ name: "selectedProject",
48
+ message: "Select a project:",
49
+ choices
50
+ }]);
51
+ (0, config_1.setConfig)("project", selectedProject);
52
+ const selectedName = projects.find((p) => p.id === selectedProject)?.name || selectedProject;
53
+ console.log(chalk_1.default.green(`✔ Default project set to '${selectedName}'`));
54
+ const currentBranch = (0, config_1.getConfigValue)("branch");
55
+ if (currentBranch) {
56
+ console.log(chalk_1.default.gray(` Active branch: ${currentBranch}`));
57
+ }
58
+ }
59
+ catch (error) {
60
+ spinner.fail("Failed to fetch projects");
61
+ if (error.response?.data?.error) {
62
+ console.error(chalk_1.default.red(`Error: ${error.response.data.error}`));
63
+ }
64
+ else {
65
+ console.error(chalk_1.default.red(error.message));
66
+ }
67
+ }
68
+ });
69
+ // GET (show current)
70
+ exports.projectCommand
71
+ .command("current")
72
+ .description("Show the current default project")
73
+ .action(async () => {
74
+ const projectId = (0, config_1.getConfigValue)("project");
75
+ const branch = (0, config_1.getConfigValue)("branch");
76
+ if (projectId) {
77
+ // Try to fetch project name
78
+ try {
79
+ const projects = await api_1.api.getProjects();
80
+ const project = projects.find((p) => p.id === projectId);
81
+ if (project) {
82
+ console.log(chalk_1.default.cyan(`Project: ${project.name} (${projectId})`));
83
+ }
84
+ else {
85
+ console.log(chalk_1.default.cyan(`Project: ${projectId}`));
86
+ }
87
+ }
88
+ catch {
89
+ console.log(chalk_1.default.cyan(`Project: ${projectId}`));
90
+ }
91
+ }
92
+ else {
93
+ console.log(chalk_1.default.yellow("No default project set. Use 'xtra project set' to set one."));
94
+ }
95
+ if (branch) {
96
+ console.log(chalk_1.default.cyan(`Branch: ${branch}`));
97
+ }
98
+ });
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rollbackCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const api_1 = require("../lib/api");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const ora_1 = __importDefault(require("ora"));
11
+ const inquirer_1 = __importDefault(require("inquirer"));
12
+ const crypto_1 = require("../lib/crypto");
13
+ const config_1 = require("../lib/config");
14
+ exports.rollbackCommand = new commander_1.Command("rollback")
15
+ .description("Rollback a secret to a previous version")
16
+ .argument("<key>", "Secret Key to rollback")
17
+ .option("-p, --project <projectId>", "Project ID")
18
+ .option("-e, --env <environment>", "Environment (dev, stg, prod)", "dev")
19
+ .action(async (key, options) => {
20
+ let { project, env } = options;
21
+ // Use config fallback
22
+ if (!project) {
23
+ project = (0, config_1.getConfigValue)("project");
24
+ }
25
+ if (!project) {
26
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or run 'xtra project set' first."));
27
+ process.exit(1);
28
+ }
29
+ // Normalize Env
30
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
31
+ env = envMap[env] || env;
32
+ const spinner = (0, ora_1.default)(`Fetching history for ${key}...`).start();
33
+ try {
34
+ const secret = await api_1.api.getSecretDetails(project, env, key);
35
+ spinner.stop();
36
+ if (!secret.history || !Array.isArray(secret.history) || secret.history.length === 0) {
37
+ console.log(chalk_1.default.yellow(`No history found for ${key}. Cannot rollback.`));
38
+ return;
39
+ }
40
+ // Prepare choices
41
+ // Current version
42
+ const choices = [];
43
+ // History versions (reverse order to show latest first)
44
+ const history = [...secret.history].reverse();
45
+ history.forEach((h) => {
46
+ let displayValue = "********";
47
+ try {
48
+ const enc = JSON.parse(h.value[0]);
49
+ displayValue = (0, crypto_1.decrypt)(enc);
50
+ }
51
+ catch (e) {
52
+ displayValue = h.value[0];
53
+ }
54
+ choices.push({
55
+ name: `v${h.version} - ${new Date(h.updatedAt).toLocaleString()} (Value: ${displayValue.substring(0, 10)}...)`,
56
+ value: h
57
+ });
58
+ });
59
+ const { selectedVersion } = await inquirer_1.default.prompt([
60
+ {
61
+ type: "list",
62
+ name: "selectedVersion",
63
+ message: "Select version to restore:",
64
+ choices: choices
65
+ }
66
+ ]);
67
+ // Restore
68
+ const confirmSpinner = (0, ora_1.default)(`Restoring version v${selectedVersion.version}...`).start();
69
+ // We use setSecrets to update it to the old value.
70
+ // This will create a NEW version in history (preserving the timeline).
71
+ let restoredValue = selectedVersion.value[0];
72
+ try {
73
+ const enc = JSON.parse(restoredValue);
74
+ restoredValue = (0, crypto_1.decrypt)(enc);
75
+ }
76
+ catch (e) { }
77
+ const payload = { [key]: restoredValue };
78
+ await api_1.api.setSecrets(project, env, payload);
79
+ confirmSpinner.succeed(`Successfully rolled back ${key} to v${selectedVersion.version}`);
80
+ // Log Audit
81
+ try {
82
+ const { logAudit } = require("../lib/audit");
83
+ logAudit("SECRET_ROLLBACK", project, env, { key, version: selectedVersion.version });
84
+ }
85
+ catch (e) { }
86
+ }
87
+ catch (error) {
88
+ spinner.fail("Failed to rollback.");
89
+ if (error.response && error.response.status === 404) {
90
+ console.error(chalk_1.default.red("Secret not found."));
91
+ }
92
+ else {
93
+ console.error(chalk_1.default.red(error.message));
94
+ }
95
+ }
96
+ });
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rotateCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const api_1 = require("../lib/api");
9
+ const config_1 = require("../lib/config");
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const ora_1 = __importDefault(require("ora"));
12
+ exports.rotateCommand = new commander_1.Command("rotate")
13
+ .description("Rotate a secret (Zero-Downtime Shadow Mode)")
14
+ .argument("<key>", "Key of the secret to rotate")
15
+ .option("-p, --project <projectId>", "Project ID")
16
+ .option("-e, --env <environment>", "Environment", "development")
17
+ .option("--strategy <strategy>", "Rotation strategy", "shadow")
18
+ .option("--promote", "Promote the shadow value to active", false)
19
+ .option("--value <value>", "New value for the secret (optional)")
20
+ .action(async (key, options) => {
21
+ let { project, env, strategy, promote, value } = options;
22
+ // Use config fallback
23
+ if (!project) {
24
+ project = (0, config_1.getConfigValue)("project");
25
+ }
26
+ // Normalize Env
27
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
28
+ env = envMap[env] || env;
29
+ if (!project) {
30
+ console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or checkout a branch."));
31
+ process.exit(1);
32
+ }
33
+ // Production confirmation gate
34
+ if (env === "production") {
35
+ const inquirer = require("inquirer");
36
+ const { confirmProd } = await inquirer.prompt([{
37
+ type: "confirm",
38
+ name: "confirmProd",
39
+ message: chalk_1.default.red(`⚠ You are about to rotate a secret in PRODUCTION (${key}). Proceed?`),
40
+ default: false,
41
+ }]);
42
+ if (!confirmProd) {
43
+ console.log(chalk_1.default.yellow("Aborted."));
44
+ return;
45
+ }
46
+ }
47
+ if (promote) {
48
+ // PROMOTE FLOW
49
+ const spinner = (0, ora_1.default)(`Promoting secret '${key}' in ${env}...`).start();
50
+ try {
51
+ const result = await api_1.api.promoteSecret(project, env, key);
52
+ spinner.succeed(chalk_1.default.green(`Successfully promoted '${key}' to version ${result.version}`));
53
+ try {
54
+ const { logAudit } = require("../lib/audit");
55
+ logAudit("SECRET_PROMOTE", project, env, { key, newVersion: result.version });
56
+ }
57
+ catch (e) { }
58
+ }
59
+ catch (error) {
60
+ spinner.fail("Promotion failed");
61
+ if (error.response && error.response.data && error.response.data.error) {
62
+ console.error(chalk_1.default.red(`Server Error: ${error.response.data.error}`));
63
+ }
64
+ else {
65
+ console.error(chalk_1.default.red(error.message));
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ // ROTATE FLOW
71
+ const spinner = (0, ora_1.default)(`Starting rotation for '${key}' (Strategy: ${strategy})...`).start();
72
+ try {
73
+ const result = await api_1.api.rotateSecret(project, env, key, strategy, value);
74
+ spinner.succeed(chalk_1.default.green(`Rotation initiated for '${key}'`));
75
+ console.log(chalk_1.default.dim(`Shadow Value: ${result.shadowValue}`));
76
+ console.log(chalk_1.default.dim(`Expires At: ${result.expiresAt}`));
77
+ console.log(chalk_1.default.blue(`\nTo verify, run your app. To finalize, run:\nxtra rotate ${key} --promote -p ${project} -e ${env}`));
78
+ try {
79
+ const { logAudit } = require("../lib/audit");
80
+ logAudit("SECRET_ROTATE", project, env, { key, strategy, expiresAt: result.expiresAt });
81
+ }
82
+ catch (e) { }
83
+ }
84
+ catch (error) {
85
+ spinner.fail("Rotation failed");
86
+ if (error.response && error.response.data && error.response.data.error) {
87
+ console.error(chalk_1.default.red(`Server Error: ${error.response.data.error}`));
88
+ }
89
+ else {
90
+ console.error(chalk_1.default.red(error.message));
91
+ }
92
+ }
93
+ }
94
+ });