@husar.ai/cli 0.4.0 → 0.4.2

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.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Auth configuration storage for Husar CLI
3
+ * Stores user credentials in ~/.husar/config.json
4
+ *
5
+ * SECURITY: Files are created with restrictive permissions (0o600)
6
+ * to prevent other users from reading sensitive tokens.
7
+ */
8
+
9
+ import { promises as fs } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ const CONFIG_DIR = join(homedir(), '.husar');
14
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
15
+
16
+ /**
17
+ * File permission modes for secure storage
18
+ * - 0o700: Directory - owner can read/write/execute, no access for others
19
+ * - 0o600: File - owner can read/write, no access for others
20
+ *
21
+ * Note: These permissions only apply on Unix-like systems (macOS, Linux).
22
+ * On Windows, Node.js ignores the mode parameter and uses ACLs instead.
23
+ */
24
+ const DIR_MODE = 0o700;
25
+ const FILE_MODE = 0o600;
26
+
27
+ export interface AuthConfig {
28
+ /** User's email address */
29
+ email?: string;
30
+ /** Access token from cloud backend */
31
+ accessToken?: string;
32
+ /** Refresh token for re-authentication */
33
+ refreshToken?: string;
34
+ /** Timestamp when the token was last refreshed */
35
+ lastRefresh?: number;
36
+ }
37
+
38
+ /**
39
+ * Cloud authentication data (JWT from husar.ai OAuth)
40
+ */
41
+ export interface CloudAuth {
42
+ /** User's email address */
43
+ email?: string;
44
+ /** JWT access token from cloud backend */
45
+ accessToken: string;
46
+ /** Refresh token for re-authentication (optional) */
47
+ refreshToken?: string;
48
+ }
49
+
50
+ export interface ProjectAuth {
51
+ /** Project name */
52
+ projectName: string;
53
+ /** CMS instance host URL */
54
+ host: string;
55
+ /** Admin token for the CMS instance */
56
+ adminToken: string;
57
+ }
58
+
59
+ export interface FullAuthConfig extends AuthConfig {
60
+ /** Cached project authentications */
61
+ projects?: Record<string, ProjectAuth>;
62
+ }
63
+
64
+ /**
65
+ * Ensure the config directory exists with secure permissions
66
+ */
67
+ async function ensureConfigDir(): Promise<void> {
68
+ try {
69
+ await fs.mkdir(CONFIG_DIR, { recursive: true, mode: DIR_MODE });
70
+ } catch (err) {
71
+ // Directory already exists - verify permissions are secure
72
+ try {
73
+ await fs.chmod(CONFIG_DIR, DIR_MODE);
74
+ } catch {
75
+ // chmod may fail on some systems, continue anyway
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Read the auth configuration
82
+ */
83
+ export async function readAuthConfig(): Promise<FullAuthConfig> {
84
+ try {
85
+ await ensureConfigDir();
86
+ const content = await fs.readFile(CONFIG_FILE, 'utf-8');
87
+ return JSON.parse(content) as FullAuthConfig;
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Write the auth configuration with secure file permissions
95
+ */
96
+ export async function writeAuthConfig(config: FullAuthConfig): Promise<void> {
97
+ await ensureConfigDir();
98
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: FILE_MODE });
99
+
100
+ // Also ensure permissions are correct for existing files
101
+ try {
102
+ await fs.chmod(CONFIG_FILE, FILE_MODE);
103
+ } catch {
104
+ // chmod may fail on some systems (e.g., Windows), continue anyway
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Check if the user is logged in
110
+ */
111
+ export async function isLoggedIn(): Promise<boolean> {
112
+ const config = await readAuthConfig();
113
+ return !!(config.accessToken && config.email);
114
+ }
115
+
116
+ /**
117
+ * Get the current user info
118
+ */
119
+ export async function getCurrentUser(): Promise<{ email: string } | null> {
120
+ const config = await readAuthConfig();
121
+ if (!config.accessToken || !config.email) {
122
+ return null;
123
+ }
124
+ return { email: config.email };
125
+ }
126
+
127
+ /**
128
+ * Save login credentials
129
+ */
130
+ export async function saveLogin(email: string, accessToken: string, refreshToken?: string): Promise<void> {
131
+ const config = await readAuthConfig();
132
+ config.email = email;
133
+ config.accessToken = accessToken;
134
+ config.refreshToken = refreshToken;
135
+ config.lastRefresh = Date.now();
136
+ await writeAuthConfig(config);
137
+ }
138
+
139
+ /**
140
+ * Save cloud authentication (JWT from OAuth)
141
+ */
142
+ export async function saveCloudAuth(cloudAuth: CloudAuth): Promise<void> {
143
+ const config = await readAuthConfig();
144
+ config.email = cloudAuth.email;
145
+ config.accessToken = cloudAuth.accessToken;
146
+ config.refreshToken = cloudAuth.refreshToken;
147
+ config.lastRefresh = Date.now();
148
+ await writeAuthConfig(config);
149
+ }
150
+
151
+ /**
152
+ * Get cloud authentication (JWT)
153
+ */
154
+ export async function getCloudAuth(): Promise<CloudAuth | null> {
155
+ const config = await readAuthConfig();
156
+ if (!config.accessToken) {
157
+ return null;
158
+ }
159
+ return {
160
+ email: config.email,
161
+ accessToken: config.accessToken,
162
+ refreshToken: config.refreshToken,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Save project authentication
168
+ */
169
+ export async function saveProjectAuth(project: ProjectAuth): Promise<void> {
170
+ const config = await readAuthConfig();
171
+ if (!config.projects) {
172
+ config.projects = {};
173
+ }
174
+ config.projects[project.projectName] = project;
175
+ await writeAuthConfig(config);
176
+ }
177
+
178
+ /**
179
+ * Get project authentication
180
+ */
181
+ export async function getProjectAuth(projectName: string): Promise<ProjectAuth | null> {
182
+ const config = await readAuthConfig();
183
+ return config.projects?.[projectName] ?? null;
184
+ }
185
+
186
+ /**
187
+ * Clear all auth data (logout)
188
+ */
189
+ export async function clearAuth(): Promise<void> {
190
+ await writeAuthConfig({});
191
+ }
192
+
193
+ /**
194
+ * Get the config file path (for display purposes)
195
+ */
196
+ export function getConfigPath(): string {
197
+ return CONFIG_FILE;
198
+ }