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.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/bin/xtra.js +124 -0
- package/dist/commands/access.js +107 -0
- package/dist/commands/admin.js +118 -0
- package/dist/commands/audit.js +67 -0
- package/dist/commands/branch.js +216 -0
- package/dist/commands/checkout.js +74 -0
- package/dist/commands/ci.js +330 -0
- package/dist/commands/completion.js +227 -0
- package/dist/commands/diff.js +163 -0
- package/dist/commands/doctor.js +176 -0
- package/dist/commands/env.js +70 -0
- package/dist/commands/export.js +84 -0
- package/dist/commands/generate.js +180 -0
- package/dist/commands/history.js +77 -0
- package/dist/commands/import.js +122 -0
- package/dist/commands/init.js +162 -0
- package/dist/commands/integration.js +188 -0
- package/dist/commands/local.js +198 -0
- package/dist/commands/login.js +176 -0
- package/dist/commands/login.test.js +51 -0
- package/dist/commands/logs.js +121 -0
- package/dist/commands/profile.js +184 -0
- package/dist/commands/project.js +98 -0
- package/dist/commands/rollback.js +96 -0
- package/dist/commands/rotate.js +94 -0
- package/dist/commands/run.js +215 -0
- package/dist/commands/scan.js +127 -0
- package/dist/commands/secrets.js +265 -0
- package/dist/commands/simulate.js +92 -0
- package/dist/commands/status.js +94 -0
- package/dist/commands/template.js +276 -0
- package/dist/commands/ui.js +218 -0
- package/dist/commands/watch.js +121 -0
- package/dist/lib/api.js +172 -0
- package/dist/lib/api.test.js +89 -0
- package/dist/lib/audit.js +136 -0
- package/dist/lib/config.js +42 -0
- package/dist/lib/config.test.js +47 -0
- package/dist/lib/crypto.js +50 -0
- package/dist/lib/manifest.js +52 -0
- package/dist/lib/profiles.js +103 -0
- 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
|
+
});
|