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.watchCommand = void 0;
7
+ /**
8
+ * watch.ts — Live reload secrets in dev mode
9
+ *
10
+ * Polls XtraSecurity Cloud for secret changes at a configurable interval.
11
+ * When a change is detected, restarts the child process with fresh secrets.
12
+ *
13
+ * Usage:
14
+ * xtra watch -p proj123 -e dev node app.js
15
+ * xtra watch -p proj123 -e dev --interval 10 npm run dev --shell
16
+ */
17
+ const commander_1 = require("commander");
18
+ const api_1 = require("../lib/api");
19
+ const config_1 = require("../lib/config");
20
+ const chalk_1 = __importDefault(require("chalk"));
21
+ const child_process_1 = require("child_process");
22
+ let child = null;
23
+ let lastHash = "";
24
+ function hashSecrets(secrets) {
25
+ return JSON.stringify(Object.keys(secrets).sort().map(k => `${k}=${secrets[k]}`));
26
+ }
27
+ async function startProcess(command, args, secrets, useShell) {
28
+ if (child) {
29
+ process.stdout.write(chalk_1.default.yellow("\n [watch] Secret change detected — restarting...\n"));
30
+ child.kill("SIGTERM");
31
+ // Give it 500ms to terminate gracefully
32
+ await new Promise(r => setTimeout(r, 500));
33
+ }
34
+ const envVars = { ...process.env, ...secrets };
35
+ process.stdout.write(chalk_1.default.green(` [watch] Starting: ${command} ${args.join(" ")}\n`));
36
+ child = (0, child_process_1.spawn)(command, args, {
37
+ env: envVars,
38
+ stdio: "inherit",
39
+ shell: useShell,
40
+ });
41
+ child.on("exit", (code) => {
42
+ if (code !== null && code !== 0) {
43
+ process.stdout.write(chalk_1.default.red(` [watch] Process exited with code ${code}\n`));
44
+ }
45
+ child = null;
46
+ });
47
+ child.on("error", (err) => {
48
+ process.stdout.write(chalk_1.default.red(` [watch] Failed to start: ${err.message}\n`));
49
+ child = null;
50
+ });
51
+ }
52
+ exports.watchCommand = new commander_1.Command("watch")
53
+ .description("Live reload — auto-restart process when secrets change in cloud")
54
+ .option("-p, --project <id>", "Project ID")
55
+ .option("-e, --env <environment>", "Environment", "development")
56
+ .option("-b, --branch <branch>", "Branch", "main")
57
+ .option("--interval <seconds>", "Poll interval in seconds", "5")
58
+ .option("--shell", "Use shell mode for the child process (for npm run etc.)", false)
59
+ .argument("<command>", "Command to run")
60
+ .argument("[args...]", "Command arguments")
61
+ .addHelpText("after", `
62
+ Examples:
63
+ $ xtra watch -p proj123 -e dev node app.js
64
+ $ xtra watch -p proj123 -e dev --interval 10 --shell npm run dev
65
+ $ xtra watch -p proj123 -e dev -- node -r dotenv/config server.js
66
+ `)
67
+ .action(async (command, args, options) => {
68
+ let { project, env, branch, interval, shell: useShell } = options;
69
+ const envMap = { dev: "development", stg: "staging", prod: "production" };
70
+ env = envMap[env] || env;
71
+ if (!project)
72
+ project = (0, config_1.getConfigValue)("project");
73
+ if (!project) {
74
+ console.error(chalk_1.default.red("Error: Project ID required. Use -p <id> or run 'xtra project set'."));
75
+ process.exit(1);
76
+ }
77
+ // Block production watch — too dangerous
78
+ if (env === "production") {
79
+ console.error(chalk_1.default.red("⚠ xtra watch is not allowed in PRODUCTION for safety reasons."));
80
+ console.error(chalk_1.default.gray(" Use xtra run for one-shot production injection."));
81
+ process.exit(1);
82
+ }
83
+ const pollMs = Math.max(3, parseInt(interval)) * 1000;
84
+ console.log(chalk_1.default.bold(`\n👁 xtra watch — watching ${env}/${branch} (every ${interval}s)\n`));
85
+ console.log(chalk_1.default.gray(` Press Ctrl+C to stop.\n`));
86
+ // Graceful shutdown
87
+ process.on("SIGINT", () => {
88
+ if (child)
89
+ child.kill("SIGTERM");
90
+ console.log(chalk_1.default.gray("\n [watch] Stopped."));
91
+ process.exit(0);
92
+ });
93
+ // Initial fetch & start
94
+ try {
95
+ const secrets = await api_1.api.getSecrets(project, env, branch);
96
+ lastHash = hashSecrets(secrets);
97
+ await startProcess(command, args, secrets, useShell);
98
+ }
99
+ catch (e) {
100
+ console.error(chalk_1.default.red("Failed to fetch secrets: " + (e?.response?.data?.error || e.message)));
101
+ process.exit(1);
102
+ }
103
+ // Poll loop
104
+ setInterval(async () => {
105
+ try {
106
+ const secrets = await api_1.api.getSecrets(project, env, branch);
107
+ const hash = hashSecrets(secrets);
108
+ if (hash !== lastHash) {
109
+ lastHash = hash;
110
+ await startProcess(command, args, secrets, useShell);
111
+ }
112
+ else {
113
+ process.stdout.write(chalk_1.default.gray(` [watch] ${new Date().toLocaleTimeString()} — no changes\r`));
114
+ }
115
+ }
116
+ catch (e) {
117
+ process.stdout.write(chalk_1.default.yellow(`\n [watch] Poll failed: ${e.message}\n`));
118
+ // Don't exit — keep trying
119
+ }
120
+ }, pollMs);
121
+ });
@@ -0,0 +1,172 @@
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.api = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const config_1 = require("./config");
9
+ const getClient = () => {
10
+ const { apiUrl } = (0, config_1.getConfig)();
11
+ const token = (0, config_1.getAuthToken)();
12
+ const client = axios_1.default.create({
13
+ baseURL: apiUrl,
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
17
+ },
18
+ });
19
+ return client;
20
+ };
21
+ exports.api = {
22
+ login: async (email, password, apiKey) => {
23
+ const response = await getClient().post("/auth/cli-login", {
24
+ email,
25
+ password,
26
+ apiKey,
27
+ });
28
+ return response.data;
29
+ },
30
+ // Placeholders for future methods
31
+ getSecrets: async (projectId, env, branch = "main") => {
32
+ // Fixed path to match Next.js dynamic route folder structure
33
+ const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets?branch=${branch}`);
34
+ return response.data;
35
+ },
36
+ getSecretVersions: async (projectId, env, branch = "main") => {
37
+ const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets?includeVersions=true&branch=${branch}`);
38
+ return response.data;
39
+ },
40
+ setSecrets: async (projectId, env, secrets, expectedVersions, branch = "main") => {
41
+ const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets`, { secrets, expectedVersions, branch });
42
+ return response.data;
43
+ },
44
+ getSecretDetails: async (projectId, env, key, branch = "main") => {
45
+ const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets/${key}?branch=${branch}`);
46
+ return response.data;
47
+ },
48
+ syncLogs: async (logs) => {
49
+ const response = await getClient().post("/audit/cli-logs", { logs });
50
+ return response.data;
51
+ },
52
+ linkSecret: async (projectId, env, key, sourceProjectId, sourceEnv, sourceKey) => {
53
+ const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/link`, {
54
+ key,
55
+ sourceProjectId,
56
+ sourceEnv,
57
+ sourceKey
58
+ });
59
+ return response.data;
60
+ },
61
+ rotateSecret: async (projectId, env, key, strategy, parsedNewValue) => {
62
+ const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/${key}/rotate`, {
63
+ strategy,
64
+ parsedNewValue
65
+ });
66
+ return response.data;
67
+ },
68
+ promoteSecret: async (projectId, env, key) => {
69
+ const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/${key}/promote`, {});
70
+ return response.data;
71
+ },
72
+ verifyAuditLogs: async () => {
73
+ const response = await getClient().get("/audit/verify");
74
+ return response.data;
75
+ },
76
+ exportAuditLogs: async (format, start, end, projectId) => {
77
+ const body = { format };
78
+ if (start)
79
+ body.startDate = start;
80
+ if (end)
81
+ body.endDate = end;
82
+ if (projectId)
83
+ body.projectId = projectId;
84
+ const response = await getClient().post("/audit/export", body, { responseType: format === "csv" ? "text" : "json" });
85
+ return response.data;
86
+ },
87
+ // JIT Access
88
+ requestAccess: async (projectId, reason, duration, secretId) => {
89
+ const response = await getClient().post("/access/request", { projectId, secretId, reason, duration });
90
+ return response.data;
91
+ },
92
+ approveAccess: async (requestId, decision) => {
93
+ const response = await getClient().post("/access/approve", { requestId, decision });
94
+ return response.data;
95
+ },
96
+ listAccessRequests: async (mode) => {
97
+ const response = await getClient().get(`/access/list?mode=${mode}`);
98
+ return response.data;
99
+ },
100
+ // Project Management
101
+ getProjects: async () => {
102
+ const response = await getClient().get("/project");
103
+ return response.data;
104
+ },
105
+ // Branch Management
106
+ getBranches: async (projectId) => {
107
+ const response = await getClient().get(`/branch?projectId=${projectId}`);
108
+ return response.data;
109
+ },
110
+ createBranch: async (projectId, name, description) => {
111
+ const response = await getClient().post("/branch", { projectId, name, description });
112
+ return response.data;
113
+ },
114
+ deleteBranch: async (branchId) => {
115
+ const response = await getClient().delete(`/branch/${branchId}`);
116
+ return response.data;
117
+ },
118
+ updateBranch: async (branchId, updates) => {
119
+ const response = await getClient().put("/branch", { id: branchId, ...updates });
120
+ return response.data;
121
+ },
122
+ // Admin - Role Management
123
+ getRoles: async () => {
124
+ const response = await getClient().get("/admin/roles");
125
+ return response.data;
126
+ },
127
+ getUsers: async (teamId) => {
128
+ const params = teamId ? `?teamId=${teamId}` : "";
129
+ const response = await getClient().get(`/admin/users${params}`);
130
+ return response.data;
131
+ },
132
+ setUserRole: async (email, role, teamId) => {
133
+ const response = await getClient().put("/admin/users/role", { email, role, teamId });
134
+ return response.data;
135
+ },
136
+ // Integrations
137
+ getIntegrationStatus: async (provider) => {
138
+ const response = await getClient().get(`/integrations/${provider}`);
139
+ return response.data;
140
+ },
141
+ getIntegrationRepos: async (provider) => {
142
+ const response = await getClient().get(`/integrations/${provider}/sync`);
143
+ return response.data.repos;
144
+ },
145
+ syncSecretsToGithub: async (data) => {
146
+ const response = await getClient().post("/api/integrations/github/sync", data);
147
+ return response.data;
148
+ },
149
+ exportKubernetesSecret: async (projectId, params) => {
150
+ const query = new URLSearchParams(params).toString();
151
+ const response = await getClient().get(`/projects/${projectId}/kubernetes?${query}`);
152
+ return response.data; // This returns the YAML string directly
153
+ },
154
+ // Advanced Features
155
+ getSecretHistory: async (projectId, env, key) => {
156
+ const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets/${key}/history`);
157
+ return response.data;
158
+ },
159
+ rollbackSecret: async (projectId, env, key, version) => {
160
+ const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/${key}/history`, { version });
161
+ return response.data;
162
+ },
163
+ cloneEnvironment: async (projectId, fromEnv, toEnv, overwrite, branch) => {
164
+ const response = await getClient().post(`/projects/${projectId}/envs/clone`, { fromEnv, toEnv, overwrite, branch });
165
+ return response.data;
166
+ },
167
+ // Generic POST method for audit logging and other uses
168
+ post: async (endpoint, data) => {
169
+ const response = await getClient().post(endpoint, data);
170
+ return response.data;
171
+ }
172
+ };
@@ -0,0 +1,89 @@
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
+ const api_1 = require("./api");
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const config_1 = require("./config");
9
+ // Mock axios and config
10
+ jest.mock('axios');
11
+ jest.mock('./config');
12
+ const mockAxios = axios_1.default;
13
+ describe('API Client', () => {
14
+ const mockCreate = jest.fn();
15
+ const mockGet = jest.fn();
16
+ const mockPost = jest.fn();
17
+ beforeAll(() => {
18
+ mockAxios.create = mockCreate;
19
+ config_1.getConfig.mockReturnValue({ apiUrl: 'http://localhost:3000/api' });
20
+ config_1.getAuthToken.mockReturnValue('test-token-123');
21
+ mockCreate.mockReturnValue({
22
+ get: mockGet,
23
+ post: mockPost,
24
+ });
25
+ });
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+ describe('login', () => {
30
+ it('should send login request with email and password', async () => {
31
+ mockPost.mockResolvedValue({ data: { token: 'new-token' } });
32
+ const result = await api_1.api.login('user@example.com', 'password123');
33
+ expect(mockPost).toHaveBeenCalledWith('/auth/cli-login', {
34
+ email: 'user@example.com',
35
+ password: 'password123',
36
+ apiKey: undefined
37
+ });
38
+ expect(result).toEqual({ token: 'new-token' });
39
+ });
40
+ it('should send login request with API key', async () => {
41
+ mockPost.mockResolvedValue({ data: { token: 'api-key-token' } });
42
+ const result = await api_1.api.login(undefined, undefined, 'xtra_key_abc123');
43
+ expect(mockPost).toHaveBeenCalledWith('/auth/cli-login', {
44
+ email: undefined,
45
+ password: undefined,
46
+ apiKey: 'xtra_key_abc123'
47
+ });
48
+ expect(result).toEqual({ token: 'api-key-token' });
49
+ });
50
+ });
51
+ describe('getSecrets', () => {
52
+ it('should fetch secrets for project and environment', async () => {
53
+ const mockSecrets = { secrets: [{ key: 'DB_URL', value: 'postgres://...' }] };
54
+ mockGet.mockResolvedValue({ data: mockSecrets });
55
+ const result = await api_1.api.getSecrets('proj-123', 'production', 'main');
56
+ expect(mockGet).toHaveBeenCalledWith('/projects/proj-123/envs/production/secrets?branch=main');
57
+ expect(result).toEqual(mockSecrets);
58
+ });
59
+ it('should use default branch if not specified', async () => {
60
+ mockGet.mockResolvedValue({ data: { secrets: [] } });
61
+ await api_1.api.getSecrets('proj-123', 'dev');
62
+ expect(mockGet).toHaveBeenCalledWith('/projects/proj-123/envs/dev/secrets?branch=main');
63
+ });
64
+ });
65
+ describe('setSecrets', () => {
66
+ it('should post secrets to API', async () => {
67
+ const secrets = { DB_URL: 'postgres://new', API_KEY: 'key123' };
68
+ mockPost.mockResolvedValue({ data: { success: true } });
69
+ const result = await api_1.api.setSecrets('proj-123', 'staging', secrets);
70
+ expect(mockPost).toHaveBeenCalledWith('/projects/proj-123/envs/staging/secrets', {
71
+ secrets,
72
+ expectedVersions: undefined,
73
+ branch: 'main'
74
+ });
75
+ expect(result).toEqual({ success: true });
76
+ });
77
+ });
78
+ describe('rotateSecret', () => {
79
+ it('should rotate secret with strategy', async () => {
80
+ mockPost.mockResolvedValue({ data: { newValue: 'rotated-value' } });
81
+ const result = await api_1.api.rotateSecret('proj-123', 'prod', 'API_KEY', 'regenerate');
82
+ expect(mockPost).toHaveBeenCalledWith('/projects/proj-123/envs/prod/secrets/API_KEY/rotate', {
83
+ strategy: 'regenerate',
84
+ parsedNewValue: undefined
85
+ });
86
+ expect(result).toEqual({ newValue: 'rotated-value' });
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,136 @@
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.logAudit = logAudit;
7
+ exports.loadAuditLogs = loadAuditLogs;
8
+ exports.saveAuditLogs = saveAuditLogs;
9
+ exports.syncAuditLogs = syncAuditLogs;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const uuid_1 = require("uuid");
13
+ const crypto_1 = require("./crypto");
14
+ const config_1 = require("./config");
15
+ const api_1 = require("./api");
16
+ const MANIFEST_DIR = path_1.default.join(process.cwd(), ".xtra");
17
+ const AUDIT_FILE = path_1.default.join(MANIFEST_DIR, "audit.enc");
18
+ /**
19
+ * Sanitize sensitive data from details
20
+ */
21
+ function sanitizeDetails(details) {
22
+ if (!details || typeof details !== 'object')
23
+ return details;
24
+ const sanitized = { ...details };
25
+ const sensitiveKeys = ['password', 'token', 'secret', 'value', 'apiKey', 'secretValue'];
26
+ for (const key of Object.keys(sanitized)) {
27
+ if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
28
+ sanitized[key] = '[REDACTED]';
29
+ }
30
+ }
31
+ return sanitized;
32
+ }
33
+ /**
34
+ * Send audit log to backend API
35
+ */
36
+ async function sendToBackend(entry) {
37
+ try {
38
+ const config = (0, config_1.getConfig)();
39
+ // Skip if not authenticated
40
+ if (!config.token) {
41
+ return false;
42
+ }
43
+ const payload = {
44
+ action: entry.action,
45
+ entity: 'cli-project',
46
+ entityId: entry.projectId || 'global',
47
+ details: sanitizeDetails(entry.details),
48
+ timestamp: entry.timestamp,
49
+ projectId: entry.projectId,
50
+ };
51
+ await api_1.api.post('/audit/cli-logs', { logs: [payload] });
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ // Silently fail - don't disrupt CLI operations
56
+ if (process.env.DEBUG) {
57
+ console.error('Failed to send audit log to backend:', error);
58
+ }
59
+ return false;
60
+ }
61
+ }
62
+ function logAudit(action, projectId, environment, details) {
63
+ const entry = {
64
+ id: (0, uuid_1.v4)(),
65
+ timestamp: new Date().toISOString(),
66
+ action,
67
+ projectId,
68
+ environment,
69
+ details,
70
+ synced: false
71
+ };
72
+ // Save locally
73
+ const logs = loadAuditLogs();
74
+ logs.push(entry);
75
+ saveAuditLogs(logs);
76
+ // Send to backend (non-blocking)
77
+ sendToBackend(entry).then((synced) => {
78
+ if (synced) {
79
+ // Mark as synced in local logs
80
+ entry.synced = true;
81
+ const updatedLogs = loadAuditLogs();
82
+ const index = updatedLogs.findIndex(l => l.id === entry.id);
83
+ if (index !== -1) {
84
+ updatedLogs[index].synced = true;
85
+ saveAuditLogs(updatedLogs);
86
+ }
87
+ }
88
+ }).catch(() => {
89
+ // Ignore errors
90
+ });
91
+ }
92
+ function loadAuditLogs() {
93
+ if (!fs_1.default.existsSync(AUDIT_FILE)) {
94
+ return [];
95
+ }
96
+ try {
97
+ const encryptedStr = fs_1.default.readFileSync(AUDIT_FILE, "utf-8");
98
+ const encryptedObj = JSON.parse(encryptedStr);
99
+ const decrypted = (0, crypto_1.decrypt)(encryptedObj);
100
+ return JSON.parse(decrypted);
101
+ }
102
+ catch (error) {
103
+ // console.error("Failed to load audit logs", error);
104
+ return [];
105
+ }
106
+ }
107
+ function saveAuditLogs(logs) {
108
+ if (!fs_1.default.existsSync(MANIFEST_DIR)) {
109
+ fs_1.default.mkdirSync(MANIFEST_DIR, { recursive: true });
110
+ }
111
+ const serialized = JSON.stringify(logs);
112
+ const encryptedObj = (0, crypto_1.encrypt)(serialized);
113
+ fs_1.default.writeFileSync(AUDIT_FILE, JSON.stringify(encryptedObj), "utf-8");
114
+ }
115
+ /**
116
+ * Sync unsynced audit logs to backend
117
+ */
118
+ async function syncAuditLogs() {
119
+ const logs = loadAuditLogs();
120
+ const unsynced = logs.filter(l => !l.synced);
121
+ if (unsynced.length === 0) {
122
+ return 0;
123
+ }
124
+ let syncedCount = 0;
125
+ for (const entry of unsynced) {
126
+ const success = await sendToBackend(entry);
127
+ if (success) {
128
+ entry.synced = true;
129
+ syncedCount++;
130
+ }
131
+ }
132
+ if (syncedCount > 0) {
133
+ saveAuditLogs(logs);
134
+ }
135
+ return syncedCount;
136
+ }
@@ -0,0 +1,42 @@
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.getProjectConfig = exports.getAuthToken = exports.clearConfig = exports.setConfig = exports.getConfigValue = exports.getConfig = void 0;
7
+ const conf_1 = __importDefault(require("conf"));
8
+ const config = new conf_1.default({
9
+ projectName: "xtra-cli",
10
+ defaults: {
11
+ apiUrl: "http://localhost:3000/api", // Default for dev
12
+ },
13
+ });
14
+ const getConfig = () => ({
15
+ ...config.store,
16
+ apiUrl: process.env.XTRA_API_URL || config.get("apiUrl") || "http://localhost:3000/api"
17
+ });
18
+ exports.getConfig = getConfig;
19
+ const getConfigValue = (key) => config.get(key);
20
+ exports.getConfigValue = getConfigValue;
21
+ const setConfig = (key, value) => config.set(key, value);
22
+ exports.setConfig = setConfig;
23
+ const clearConfig = () => config.clear();
24
+ exports.clearConfig = clearConfig;
25
+ const getAuthToken = () => config.get("token");
26
+ exports.getAuthToken = getAuthToken;
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const path_1 = __importDefault(require("path"));
29
+ const getProjectConfig = async () => {
30
+ try {
31
+ const configPath = path_1.default.join(process.cwd(), "xtra.json");
32
+ if (fs_1.default.existsSync(configPath)) {
33
+ const content = fs_1.default.readFileSync(configPath, "utf-8");
34
+ return JSON.parse(content);
35
+ }
36
+ return null;
37
+ }
38
+ catch (error) {
39
+ return null;
40
+ }
41
+ };
42
+ exports.getProjectConfig = getProjectConfig;
@@ -0,0 +1,47 @@
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
+ const config_1 = require("./config");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ // Mock Conf module
9
+ jest.mock('conf');
10
+ describe('Config Management', () => {
11
+ beforeEach(() => {
12
+ jest.clearAllMocks();
13
+ });
14
+ describe('getConfig', () => {
15
+ it('should return config with API URL from env if set', () => {
16
+ process.env.XTRA_API_URL = 'https://api.production.com';
17
+ const config = (0, config_1.getConfig)();
18
+ expect(config.apiUrl).toBe('https://api.production.com');
19
+ delete process.env.XTRA_API_URL;
20
+ });
21
+ it('should fallback to default API URL', () => {
22
+ delete process.env.XTRA_API_URL;
23
+ const config = (0, config_1.getConfig)();
24
+ expect(config.apiUrl).toBeDefined();
25
+ });
26
+ });
27
+ describe('getProjectConfig', () => {
28
+ it('should return null if xtra.json does not exist', async () => {
29
+ jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(false);
30
+ const projectConfig = await (0, config_1.getProjectConfig)();
31
+ expect(projectConfig).toBeNull();
32
+ });
33
+ it('should parse and return xtra.json if it exists', async () => {
34
+ const mockConfig = { projectId: 'test-project', env: 'production' };
35
+ jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(true);
36
+ jest.spyOn(fs_1.default, 'readFileSync').mockReturnValue(JSON.stringify(mockConfig));
37
+ const projectConfig = await (0, config_1.getProjectConfig)();
38
+ expect(projectConfig).toEqual(mockConfig);
39
+ });
40
+ it('should return null if JSON parsing fails', async () => {
41
+ jest.spyOn(fs_1.default, 'existsSync').mockReturnValue(true);
42
+ jest.spyOn(fs_1.default, 'readFileSync').mockReturnValue('invalid json{');
43
+ const projectConfig = await (0, config_1.getProjectConfig)();
44
+ expect(projectConfig).toBeNull();
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,50 @@
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.hash = exports.decrypt = exports.encrypt = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const conf_1 = __importDefault(require("conf"));
9
+ // We'll use the machine-id or a generated master key stored in the system keychain
10
+ // (or simple config for MVP) to encrypt the local cache.
11
+ // For now, we will generate a random key and store it in the config (simulate keychain).
12
+ const algorithm = "aes-256-gcm";
13
+ const config = new conf_1.default({ projectName: "xtra-cli-crypto" });
14
+ function getMasterKey() {
15
+ let keyHex = config.get("masterKey");
16
+ if (!keyHex) {
17
+ keyHex = crypto_1.default.randomBytes(32).toString("hex");
18
+ config.set("masterKey", keyHex);
19
+ }
20
+ return Buffer.from(keyHex, "hex");
21
+ }
22
+ const encrypt = (text) => {
23
+ const iv = crypto_1.default.randomBytes(16);
24
+ const key = getMasterKey();
25
+ const cipher = crypto_1.default.createCipheriv(algorithm, key, iv);
26
+ let encrypted = cipher.update(text, "utf8", "hex");
27
+ encrypted += cipher.final("hex");
28
+ const tag = cipher.getAuthTag();
29
+ return {
30
+ iv: iv.toString("hex"),
31
+ content: encrypted,
32
+ tag: tag.toString("hex"),
33
+ };
34
+ };
35
+ exports.encrypt = encrypt;
36
+ const decrypt = (encrypted) => {
37
+ const key = getMasterKey();
38
+ const iv = Buffer.from(encrypted.iv, "hex");
39
+ const tag = Buffer.from(encrypted.tag, "hex");
40
+ const decipher = crypto_1.default.createDecipheriv(algorithm, key, iv);
41
+ decipher.setAuthTag(tag);
42
+ let decrypted = decipher.update(encrypted.content, "hex", "utf8");
43
+ decrypted += decipher.final("utf8");
44
+ return decrypted;
45
+ };
46
+ exports.decrypt = decrypt;
47
+ const hash = (text) => {
48
+ return crypto_1.default.createHash("sha256").update(text).digest("hex");
49
+ };
50
+ exports.hash = hash;