spck 0.3.1
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/.oxlintrc.json +49 -0
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/bin/cli.js +20 -0
- package/bin/validate-cwd.js +41 -0
- package/dist/config/__tests__/config.test.d.ts +2 -0
- package/dist/config/__tests__/config.test.js +262 -0
- package/dist/config/__tests__/credentials.test.d.ts +2 -0
- package/dist/config/__tests__/credentials.test.js +360 -0
- package/dist/config/config.d.ts +33 -0
- package/dist/config/config.js +185 -0
- package/dist/config/credentials.d.ts +75 -0
- package/dist/config/credentials.js +259 -0
- package/dist/config/server-selection.d.ts +40 -0
- package/dist/config/server-selection.js +130 -0
- package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
- package/dist/connection/__tests__/firebase-auth.test.js +96 -0
- package/dist/connection/__tests__/hmac.test.d.ts +2 -0
- package/dist/connection/__tests__/hmac.test.js +372 -0
- package/dist/connection/auth.d.ts +13 -0
- package/dist/connection/auth.js +91 -0
- package/dist/connection/firebase-auth.d.ts +40 -0
- package/dist/connection/firebase-auth.js +429 -0
- package/dist/connection/hmac.d.ts +24 -0
- package/dist/connection/hmac.js +109 -0
- package/dist/i18n/index.d.ts +25 -0
- package/dist/i18n/index.js +101 -0
- package/dist/i18n/locales/en.json +313 -0
- package/dist/i18n/locales/es.json +302 -0
- package/dist/i18n/locales/fr.json +302 -0
- package/dist/i18n/locales/id.json +302 -0
- package/dist/i18n/locales/ja.json +302 -0
- package/dist/i18n/locales/ko.json +302 -0
- package/dist/i18n/locales/locales/en.json +309 -0
- package/dist/i18n/locales/locales/es.json +302 -0
- package/dist/i18n/locales/locales/fr.json +302 -0
- package/dist/i18n/locales/locales/id.json +302 -0
- package/dist/i18n/locales/locales/ja.json +302 -0
- package/dist/i18n/locales/locales/ko.json +302 -0
- package/dist/i18n/locales/locales/pt.json +302 -0
- package/dist/i18n/locales/locales/zh-Hans.json +302 -0
- package/dist/i18n/locales/pt.json +302 -0
- package/dist/i18n/locales/zh-Hans.json +302 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +493 -0
- package/dist/proxy/ProxyClient.d.ts +125 -0
- package/dist/proxy/ProxyClient.js +781 -0
- package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
- package/dist/proxy/ProxySocketWrapper.js +98 -0
- package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
- package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
- package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
- package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
- package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
- package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
- package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
- package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
- package/dist/proxy/chunking.d.ts +53 -0
- package/dist/proxy/chunking.js +127 -0
- package/dist/proxy/handshake-validation.d.ts +21 -0
- package/dist/proxy/handshake-validation.js +49 -0
- package/dist/rpc/__tests__/router.test.d.ts +2 -0
- package/dist/rpc/__tests__/router.test.js +262 -0
- package/dist/rpc/router.d.ts +37 -0
- package/dist/rpc/router.js +132 -0
- package/dist/services/BrowserProxyService.d.ts +13 -0
- package/dist/services/BrowserProxyService.js +139 -0
- package/dist/services/FilesystemService.d.ts +99 -0
- package/dist/services/FilesystemService.js +742 -0
- package/dist/services/GitService.d.ts +243 -0
- package/dist/services/GitService.js +1439 -0
- package/dist/services/SearchService.d.ts +93 -0
- package/dist/services/SearchService.js +670 -0
- package/dist/services/TerminalService.d.ts +62 -0
- package/dist/services/TerminalService.js +337 -0
- package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
- package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
- package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
- package/dist/services/__tests__/FilesystemService.test.js +609 -0
- package/dist/services/__tests__/GitService.test.d.ts +2 -0
- package/dist/services/__tests__/GitService.test.js +953 -0
- package/dist/services/__tests__/SearchService.test.d.ts +2 -0
- package/dist/services/__tests__/SearchService.test.js +384 -0
- package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
- package/dist/services/__tests__/TerminalService.test.js +513 -0
- package/dist/setup/wizard.d.ts +10 -0
- package/dist/setup/wizard.js +172 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.js +44 -0
- package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
- package/dist/utils/__tests__/gitignore.test.js +127 -0
- package/dist/utils/gitignore.d.ts +24 -0
- package/dist/utils/gitignore.js +77 -0
- package/dist/utils/logger.d.ts +96 -0
- package/dist/utils/logger.js +456 -0
- package/dist/utils/project-dir.d.ts +51 -0
- package/dist/utils/project-dir.js +191 -0
- package/dist/utils/ripgrep.d.ts +34 -0
- package/dist/utils/ripgrep.js +148 -0
- package/dist/utils/tool-detection.d.ts +17 -0
- package/dist/utils/tool-detection.js +126 -0
- package/dist/watcher/FileWatcher.d.ts +10 -0
- package/dist/watcher/FileWatcher.js +42 -0
- package/package.json +70 -0
- package/src/config/__tests__/config.test.ts +318 -0
- package/src/config/__tests__/credentials.test.ts +494 -0
- package/src/config/config.ts +206 -0
- package/src/config/credentials.ts +302 -0
- package/src/config/server-selection.ts +150 -0
- package/src/connection/__tests__/firebase-auth.test.ts +121 -0
- package/src/connection/__tests__/hmac.test.ts +509 -0
- package/src/connection/auth.ts +140 -0
- package/src/connection/firebase-auth.ts +504 -0
- package/src/connection/hmac.ts +139 -0
- package/src/i18n/index.ts +119 -0
- package/src/i18n/locales/en.json +313 -0
- package/src/i18n/locales/es.json +302 -0
- package/src/i18n/locales/fr.json +302 -0
- package/src/i18n/locales/id.json +302 -0
- package/src/i18n/locales/ja.json +302 -0
- package/src/i18n/locales/ko.json +302 -0
- package/src/i18n/locales/pt.json +302 -0
- package/src/i18n/locales/zh-Hans.json +302 -0
- package/src/index.ts +542 -0
- package/src/proxy/ProxyClient.ts +968 -0
- package/src/proxy/ProxySocketWrapper.ts +113 -0
- package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
- package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
- package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
- package/src/proxy/chunking.ts +162 -0
- package/src/proxy/handshake-validation.ts +64 -0
- package/src/rpc/__tests__/router.test.ts +400 -0
- package/src/rpc/router.ts +183 -0
- package/src/services/BrowserProxyService.ts +179 -0
- package/src/services/FilesystemService.ts +841 -0
- package/src/services/GitService.ts +1639 -0
- package/src/services/SearchService.ts +809 -0
- package/src/services/TerminalService.ts +413 -0
- package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
- package/src/services/__tests__/FilesystemService.test.ts +1002 -0
- package/src/services/__tests__/GitService.test.ts +1552 -0
- package/src/services/__tests__/SearchService.test.ts +484 -0
- package/src/services/__tests__/TerminalService.test.ts +702 -0
- package/src/setup/wizard.ts +242 -0
- package/src/types/fossil-delta.d.ts +4 -0
- package/src/types.ts +287 -0
- package/src/utils/__tests__/gitignore.test.ts +174 -0
- package/src/utils/gitignore.ts +91 -0
- package/src/utils/logger.ts +578 -0
- package/src/utils/project-dir.ts +218 -0
- package/src/utils/ripgrep.ts +180 -0
- package/src/utils/tool-detection.ts +141 -0
- package/src/watcher/FileWatcher.ts +53 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for credentials management
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import { loadCredentials, saveCredentials, loadConnectionSettings, saveConnectionSettings, isServerTokenExpired, getCredentialsPath, getConnectionSettingsPath, getGlobalConfigPath, loadGlobalConfig, saveGlobalConfig, clearCredentials, clearConnectionSettings, } from '../credentials.js';
|
|
8
|
+
// Mock modules
|
|
9
|
+
vi.mock('fs');
|
|
10
|
+
vi.mock('os');
|
|
11
|
+
const mockFs = fs;
|
|
12
|
+
const mockOs = os;
|
|
13
|
+
describe('credentials', () => {
|
|
14
|
+
const mockHomedir = '/mock/home';
|
|
15
|
+
const mockCwd = '/mock/project';
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
mockOs.homedir.mockReturnValue(mockHomedir);
|
|
19
|
+
vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
describe('getCredentialsPath()', () => {
|
|
25
|
+
it('should return path in user home directory', () => {
|
|
26
|
+
const path = getCredentialsPath();
|
|
27
|
+
expect(path).toBe(`${mockHomedir}/.spck-editor/.credentials.json`);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('getConnectionSettingsPath()', () => {
|
|
31
|
+
it('should return path in .spck-editor/config subdirectory', () => {
|
|
32
|
+
const path = getConnectionSettingsPath();
|
|
33
|
+
expect(path).toBe(`${mockCwd}/.spck-editor/config/connection-settings.json`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('loadCredentials()', () => {
|
|
37
|
+
it('should return null if file does not exist', () => {
|
|
38
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
39
|
+
const result = loadCredentials();
|
|
40
|
+
expect(result).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
it('should load and return valid stored credentials', () => {
|
|
43
|
+
const mockCredentials = {
|
|
44
|
+
refreshToken: 'mock-refresh-token',
|
|
45
|
+
userId: 'user-123',
|
|
46
|
+
};
|
|
47
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
48
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockCredentials));
|
|
49
|
+
const result = loadCredentials();
|
|
50
|
+
expect(result).toEqual(mockCredentials);
|
|
51
|
+
});
|
|
52
|
+
it('should throw CORRUPTED error for invalid JSON', () => {
|
|
53
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
54
|
+
mockFs.readFileSync.mockReturnValue('invalid json{');
|
|
55
|
+
expect(() => loadCredentials()).toThrow('Credentials file is corrupted');
|
|
56
|
+
expect(() => {
|
|
57
|
+
try {
|
|
58
|
+
loadCredentials();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
expect(error.code).toBe('CORRUPTED');
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}).toThrow();
|
|
65
|
+
});
|
|
66
|
+
it('should throw CORRUPTED error for missing refreshToken', () => {
|
|
67
|
+
const invalidCredentials = {
|
|
68
|
+
userId: 'user-123',
|
|
69
|
+
};
|
|
70
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
71
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(invalidCredentials));
|
|
72
|
+
expect(() => {
|
|
73
|
+
try {
|
|
74
|
+
loadCredentials();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
expect(error.code).toBe('CORRUPTED');
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}).toThrow();
|
|
81
|
+
});
|
|
82
|
+
it('should throw CORRUPTED error for missing userId', () => {
|
|
83
|
+
const invalidCredentials = {
|
|
84
|
+
refreshToken: 'mock-refresh-token',
|
|
85
|
+
};
|
|
86
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
87
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(invalidCredentials));
|
|
88
|
+
expect(() => {
|
|
89
|
+
try {
|
|
90
|
+
loadCredentials();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
expect(error.code).toBe('CORRUPTED');
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}).toThrow();
|
|
97
|
+
});
|
|
98
|
+
it('should only return refreshToken and userId even if file has extra fields', () => {
|
|
99
|
+
const storedWithExtra = {
|
|
100
|
+
refreshToken: 'mock-refresh-token',
|
|
101
|
+
userId: 'user-123',
|
|
102
|
+
firebaseToken: 'old-token',
|
|
103
|
+
firebaseTokenExpiry: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
106
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(storedWithExtra));
|
|
107
|
+
const result = loadCredentials();
|
|
108
|
+
// Should only return the stored credentials fields
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
refreshToken: 'mock-refresh-token',
|
|
111
|
+
userId: 'user-123',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('saveCredentials()', () => {
|
|
116
|
+
const mockCredentials = {
|
|
117
|
+
refreshToken: 'mock-refresh-token',
|
|
118
|
+
userId: 'user-123',
|
|
119
|
+
};
|
|
120
|
+
it('should create directory if it does not exist', () => {
|
|
121
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
122
|
+
mockFs.mkdirSync.mockReturnValue(undefined);
|
|
123
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
124
|
+
saveCredentials(mockCredentials);
|
|
125
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(`${mockHomedir}/.spck-editor`, { recursive: true, mode: 0o700 });
|
|
126
|
+
});
|
|
127
|
+
it('should write only refreshToken and userId with correct permissions', () => {
|
|
128
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
129
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
130
|
+
saveCredentials(mockCredentials);
|
|
131
|
+
// Should only persist refreshToken + userId
|
|
132
|
+
const expectedStored = {
|
|
133
|
+
refreshToken: 'mock-refresh-token',
|
|
134
|
+
userId: 'user-123',
|
|
135
|
+
};
|
|
136
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(`${mockHomedir}/.spck-editor/.credentials.json`, JSON.stringify(expectedStored, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
137
|
+
});
|
|
138
|
+
it('should throw error with operation context on write failure', () => {
|
|
139
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
140
|
+
const mockError = new Error('ENOSPC: no space left');
|
|
141
|
+
mockError.code = 'ENOSPC';
|
|
142
|
+
mockFs.writeFileSync.mockImplementation(() => {
|
|
143
|
+
throw mockError;
|
|
144
|
+
});
|
|
145
|
+
expect(() => {
|
|
146
|
+
try {
|
|
147
|
+
saveCredentials(mockCredentials);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
expect(error.operation).toBe('save credentials');
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}).toThrow();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('loadConnectionSettings()', () => {
|
|
157
|
+
it('should return null if file does not exist', () => {
|
|
158
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
159
|
+
const result = loadConnectionSettings();
|
|
160
|
+
expect(result).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
it('should load and return valid connection settings', () => {
|
|
163
|
+
const mockSettings = {
|
|
164
|
+
serverToken: 'server-token',
|
|
165
|
+
serverTokenExpiry: Date.now() + 3600000,
|
|
166
|
+
clientId: 'client-123',
|
|
167
|
+
secret: 'secret-abc',
|
|
168
|
+
userId: 'user-123',
|
|
169
|
+
connectedAt: Date.now(),
|
|
170
|
+
};
|
|
171
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
172
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockSettings));
|
|
173
|
+
const result = loadConnectionSettings();
|
|
174
|
+
expect(result).toEqual(mockSettings);
|
|
175
|
+
});
|
|
176
|
+
it('should throw CORRUPTED error for invalid JSON', () => {
|
|
177
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
178
|
+
mockFs.readFileSync.mockReturnValue('not valid json');
|
|
179
|
+
expect(() => {
|
|
180
|
+
try {
|
|
181
|
+
loadConnectionSettings();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
expect(error.code).toBe('CORRUPTED');
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}).toThrow();
|
|
188
|
+
});
|
|
189
|
+
it('should throw CORRUPTED error for missing required fields', () => {
|
|
190
|
+
const invalidSettings = {
|
|
191
|
+
serverToken: 'token',
|
|
192
|
+
// missing clientId and secret
|
|
193
|
+
};
|
|
194
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
195
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(invalidSettings));
|
|
196
|
+
expect(() => {
|
|
197
|
+
try {
|
|
198
|
+
loadConnectionSettings();
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
expect(error.code).toBe('CORRUPTED');
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}).toThrow();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('saveConnectionSettings()', () => {
|
|
208
|
+
const mockSettings = {
|
|
209
|
+
serverToken: 'server-token',
|
|
210
|
+
serverTokenExpiry: Date.now() + 3600000,
|
|
211
|
+
clientId: 'client-123',
|
|
212
|
+
secret: 'secret-abc',
|
|
213
|
+
userId: 'user-123',
|
|
214
|
+
connectedAt: Date.now(),
|
|
215
|
+
};
|
|
216
|
+
it('should create directory if it does not exist', () => {
|
|
217
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
218
|
+
mockFs.mkdirSync.mockReturnValue(undefined);
|
|
219
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
220
|
+
saveConnectionSettings(mockSettings);
|
|
221
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(`${mockCwd}/.spck-editor/config`, { recursive: true, mode: 0o700 });
|
|
222
|
+
});
|
|
223
|
+
it('should write settings file with restricted permissions', () => {
|
|
224
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
225
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
226
|
+
saveConnectionSettings(mockSettings);
|
|
227
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(`${mockCwd}/.spck-editor/config/connection-settings.json`, JSON.stringify(mockSettings, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
228
|
+
});
|
|
229
|
+
it('should throw error with operation context on write failure', () => {
|
|
230
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
231
|
+
const mockError = new Error('EACCES: permission denied');
|
|
232
|
+
mockError.code = 'EACCES';
|
|
233
|
+
mockFs.writeFileSync.mockImplementation(() => {
|
|
234
|
+
throw mockError;
|
|
235
|
+
});
|
|
236
|
+
expect(() => {
|
|
237
|
+
try {
|
|
238
|
+
saveConnectionSettings(mockSettings);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
expect(error.operation).toBe('save connection settings');
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}).toThrow();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe('isServerTokenExpired()', () => {
|
|
248
|
+
it('should return true if settings is null', () => {
|
|
249
|
+
expect(isServerTokenExpired(null)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
it('should return true if serverTokenExpiry is missing', () => {
|
|
252
|
+
expect(isServerTokenExpired({ serverToken: 'token' })).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
it('should return true if token has expired', () => {
|
|
255
|
+
const expiredSettings = {
|
|
256
|
+
serverToken: 'token',
|
|
257
|
+
serverTokenExpiry: Date.now() - 1000,
|
|
258
|
+
};
|
|
259
|
+
expect(isServerTokenExpired(expiredSettings)).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
it('should return false if token is still valid', () => {
|
|
262
|
+
const validSettings = {
|
|
263
|
+
serverToken: 'token',
|
|
264
|
+
serverTokenExpiry: Date.now() + 3600000,
|
|
265
|
+
};
|
|
266
|
+
expect(isServerTokenExpired(validSettings)).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe('clearCredentials()', () => {
|
|
270
|
+
it('should delete credentials file if it exists', () => {
|
|
271
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
272
|
+
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
273
|
+
clearCredentials();
|
|
274
|
+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(`${mockHomedir}/.spck-editor/.credentials.json`);
|
|
275
|
+
});
|
|
276
|
+
it('should do nothing if file does not exist', () => {
|
|
277
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
278
|
+
clearCredentials();
|
|
279
|
+
expect(mockFs.unlinkSync).not.toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('clearConnectionSettings()', () => {
|
|
283
|
+
it('should delete settings file if it exists', () => {
|
|
284
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
285
|
+
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
286
|
+
clearConnectionSettings();
|
|
287
|
+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(`${mockCwd}/.spck-editor/config/connection-settings.json`);
|
|
288
|
+
});
|
|
289
|
+
it('should do nothing if file does not exist', () => {
|
|
290
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
291
|
+
clearConnectionSettings();
|
|
292
|
+
expect(mockFs.unlinkSync).not.toHaveBeenCalled();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
describe('getGlobalConfigPath()', () => {
|
|
296
|
+
it('should return path in user home directory', () => {
|
|
297
|
+
const configPath = getGlobalConfigPath();
|
|
298
|
+
expect(configPath).toBe(`${mockHomedir}/.spck-editor/global.config`);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
describe('loadGlobalConfig()', () => {
|
|
302
|
+
it('should return default config if file does not exist', () => {
|
|
303
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
304
|
+
const result = loadGlobalConfig();
|
|
305
|
+
expect(result).toEqual({ knownDeviceIds: [] });
|
|
306
|
+
});
|
|
307
|
+
it('should load and return valid global config', () => {
|
|
308
|
+
const mockConfig = {
|
|
309
|
+
knownDeviceIds: ['device-aaa', 'device-bbb'],
|
|
310
|
+
};
|
|
311
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
312
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));
|
|
313
|
+
const result = loadGlobalConfig();
|
|
314
|
+
expect(result).toEqual(mockConfig);
|
|
315
|
+
});
|
|
316
|
+
it('should return default config if JSON is invalid', () => {
|
|
317
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
318
|
+
mockFs.readFileSync.mockReturnValue('not valid json{');
|
|
319
|
+
const result = loadGlobalConfig();
|
|
320
|
+
expect(result).toEqual({ knownDeviceIds: [] });
|
|
321
|
+
});
|
|
322
|
+
it('should return default config if knownDeviceIds is not an array', () => {
|
|
323
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
324
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ knownDeviceIds: 'bad' }));
|
|
325
|
+
const result = loadGlobalConfig();
|
|
326
|
+
expect(result).toEqual({ knownDeviceIds: [] });
|
|
327
|
+
});
|
|
328
|
+
it('should return empty knownDeviceIds if field is missing', () => {
|
|
329
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
330
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({}));
|
|
331
|
+
const result = loadGlobalConfig();
|
|
332
|
+
expect(result).toEqual({ knownDeviceIds: [] });
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
describe('saveGlobalConfig()', () => {
|
|
336
|
+
const mockConfig = {
|
|
337
|
+
knownDeviceIds: ['device-aaa', 'device-bbb'],
|
|
338
|
+
};
|
|
339
|
+
it('should create directory if it does not exist', () => {
|
|
340
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
341
|
+
mockFs.mkdirSync.mockReturnValue(undefined);
|
|
342
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
343
|
+
saveGlobalConfig(mockConfig);
|
|
344
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(`${mockHomedir}/.spck-editor`, { recursive: true, mode: 0o700 });
|
|
345
|
+
});
|
|
346
|
+
it('should write config file with correct content and restricted permissions', () => {
|
|
347
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
348
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
349
|
+
saveGlobalConfig(mockConfig);
|
|
350
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(`${mockHomedir}/.spck-editor/global.config`, JSON.stringify(mockConfig, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
351
|
+
});
|
|
352
|
+
it('should not create directory if it already exists', () => {
|
|
353
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
354
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
355
|
+
saveGlobalConfig(mockConfig);
|
|
356
|
+
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
//# sourceMappingURL=credentials.test.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for spck-cli server
|
|
3
|
+
*/
|
|
4
|
+
import { ServerConfig } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Load server configuration from file
|
|
7
|
+
* If config file doesn't exist, runs setup wizard
|
|
8
|
+
* @throws {ConfigNotFoundError} if file doesn't exist
|
|
9
|
+
* @throws {Error} with code 'CORRUPTED' if file is corrupted
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadConfig(configPath?: string): ServerConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Custom error for missing configuration
|
|
14
|
+
*/
|
|
15
|
+
export declare class ConfigNotFoundError extends Error {
|
|
16
|
+
configPath: string;
|
|
17
|
+
constructor(configPath: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Save server configuration to file
|
|
21
|
+
* @throws {Error} with code 'EACCES' for permission errors
|
|
22
|
+
* @throws {Error} with code 'ENOSPC' for disk full errors
|
|
23
|
+
*/
|
|
24
|
+
export declare function saveConfig(config: ServerConfig, configPath?: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Create default configuration template
|
|
27
|
+
*/
|
|
28
|
+
export declare function createDefaultConfig(overrides?: Partial<ServerConfig>): ServerConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Parse file size string to bytes
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseFileSize(sizeStr: string): number;
|
|
33
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for spck-cli server
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { getProjectFilePath } from '../utils/project-dir.js';
|
|
7
|
+
import { t } from '../i18n/index.js';
|
|
8
|
+
const DEFAULT_CONFIG_FILENAME = 'spck-cli.config.json';
|
|
9
|
+
/**
|
|
10
|
+
* Load server configuration from file
|
|
11
|
+
* If config file doesn't exist, runs setup wizard
|
|
12
|
+
* @throws {ConfigNotFoundError} if file doesn't exist
|
|
13
|
+
* @throws {Error} with code 'CORRUPTED' if file is corrupted
|
|
14
|
+
*/
|
|
15
|
+
export function loadConfig(configPath) {
|
|
16
|
+
// If a custom config path is provided, use it as-is
|
|
17
|
+
// Otherwise use the default location in the project directory
|
|
18
|
+
const fullPath = configPath
|
|
19
|
+
? path.resolve(process.cwd(), configPath)
|
|
20
|
+
: getProjectFilePath(process.cwd(), DEFAULT_CONFIG_FILENAME);
|
|
21
|
+
if (!fs.existsSync(fullPath)) {
|
|
22
|
+
console.log('\n' + t('config.fileNotFound', { path: fullPath }));
|
|
23
|
+
console.log(t('config.fileNotFoundHint') + '\n');
|
|
24
|
+
// Signal to caller that setup is needed
|
|
25
|
+
throw new ConfigNotFoundError(fullPath);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const configData = fs.readFileSync(fullPath, 'utf8');
|
|
29
|
+
const config = JSON.parse(configData);
|
|
30
|
+
// Validate required fields
|
|
31
|
+
validateConfig(config);
|
|
32
|
+
// Backward-compatibility: populate missing optional sections and re-save
|
|
33
|
+
let needsSave = false;
|
|
34
|
+
if (!config.browserProxy) {
|
|
35
|
+
config.browserProxy = { enabled: true };
|
|
36
|
+
needsSave = true;
|
|
37
|
+
}
|
|
38
|
+
if (needsSave) {
|
|
39
|
+
try {
|
|
40
|
+
saveConfig(config, configPath);
|
|
41
|
+
}
|
|
42
|
+
catch { /* best-effort */ }
|
|
43
|
+
}
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// JSON parse error or validation error
|
|
48
|
+
if (error instanceof SyntaxError || (error.message && error.message.includes('Invalid'))) {
|
|
49
|
+
console.warn('⚠️ ' + t('config.fileCorrupted', { path: fullPath }));
|
|
50
|
+
console.warn(' ' + t('config.fileCorruptedHint') + '\n');
|
|
51
|
+
const corruptedError = new Error('Configuration file is corrupted');
|
|
52
|
+
corruptedError.code = 'CORRUPTED';
|
|
53
|
+
corruptedError.path = fullPath;
|
|
54
|
+
corruptedError.originalError = error;
|
|
55
|
+
throw corruptedError;
|
|
56
|
+
}
|
|
57
|
+
// Other errors (permission, etc.)
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Custom error for missing configuration
|
|
63
|
+
*/
|
|
64
|
+
export class ConfigNotFoundError extends Error {
|
|
65
|
+
constructor(configPath) {
|
|
66
|
+
super(`Configuration file not found: ${configPath}`);
|
|
67
|
+
this.configPath = configPath;
|
|
68
|
+
this.name = 'ConfigNotFoundError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Save server configuration to file
|
|
73
|
+
* @throws {Error} with code 'EACCES' for permission errors
|
|
74
|
+
* @throws {Error} with code 'ENOSPC' for disk full errors
|
|
75
|
+
*/
|
|
76
|
+
export function saveConfig(config, configPath) {
|
|
77
|
+
// If a custom config path is provided, use it as-is
|
|
78
|
+
// Otherwise use the default location in the project directory
|
|
79
|
+
const fullPath = configPath
|
|
80
|
+
? path.resolve(process.cwd(), configPath)
|
|
81
|
+
: getProjectFilePath(process.cwd(), DEFAULT_CONFIG_FILENAME);
|
|
82
|
+
try {
|
|
83
|
+
// Ensure directory exists
|
|
84
|
+
const dir = path.dirname(fullPath);
|
|
85
|
+
if (!fs.existsSync(dir)) {
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
// Validate before saving
|
|
89
|
+
validateConfig(config);
|
|
90
|
+
// Write config file
|
|
91
|
+
fs.writeFileSync(fullPath, JSON.stringify(config, null, 2), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Add context to error
|
|
95
|
+
error.path = error.path || fullPath;
|
|
96
|
+
error.operation = 'save config';
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate configuration
|
|
102
|
+
*/
|
|
103
|
+
function validateConfig(config) {
|
|
104
|
+
if (!config.version || config.version !== 1) {
|
|
105
|
+
throw new Error('Invalid config version. Expected version 1.');
|
|
106
|
+
}
|
|
107
|
+
if (!config.root || typeof config.root !== 'string') {
|
|
108
|
+
throw new Error('Invalid or missing root directory in configuration.');
|
|
109
|
+
}
|
|
110
|
+
if (!fs.existsSync(config.root)) {
|
|
111
|
+
throw new Error(`Root directory does not exist: ${config.root}`);
|
|
112
|
+
}
|
|
113
|
+
if (!config.terminal || typeof config.terminal !== 'object') {
|
|
114
|
+
throw new Error('Invalid or missing terminal configuration.');
|
|
115
|
+
}
|
|
116
|
+
if (typeof config.terminal.enabled !== 'boolean') {
|
|
117
|
+
throw new Error('Terminal enabled must be a boolean.');
|
|
118
|
+
}
|
|
119
|
+
if (!config.security || typeof config.security !== 'object') {
|
|
120
|
+
throw new Error('Invalid or missing security configuration.');
|
|
121
|
+
}
|
|
122
|
+
if (!config.filesystem || typeof config.filesystem !== 'object') {
|
|
123
|
+
throw new Error('Invalid or missing filesystem configuration.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get current directory name for default server name
|
|
128
|
+
*/
|
|
129
|
+
function getDefaultServerName() {
|
|
130
|
+
const cwd = process.cwd();
|
|
131
|
+
return path.basename(cwd);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Create default configuration template
|
|
135
|
+
*/
|
|
136
|
+
export function createDefaultConfig(overrides = {}) {
|
|
137
|
+
return {
|
|
138
|
+
version: 1,
|
|
139
|
+
root: process.cwd(),
|
|
140
|
+
name: getDefaultServerName(),
|
|
141
|
+
terminal: {
|
|
142
|
+
enabled: true,
|
|
143
|
+
maxBufferedLines: 5000,
|
|
144
|
+
maxTerminals: 10,
|
|
145
|
+
},
|
|
146
|
+
security: {
|
|
147
|
+
userAuthenticationEnabled: false,
|
|
148
|
+
},
|
|
149
|
+
filesystem: {
|
|
150
|
+
maxFileSize: '10MB',
|
|
151
|
+
watchIgnorePatterns: [
|
|
152
|
+
'**/.git/**',
|
|
153
|
+
'**/.spck-editor/**',
|
|
154
|
+
'**/node_modules/**',
|
|
155
|
+
'**/*.log',
|
|
156
|
+
'**/.DS_Store',
|
|
157
|
+
'**/dist/**',
|
|
158
|
+
'**/build/**'
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
browserProxy: {
|
|
162
|
+
enabled: true,
|
|
163
|
+
},
|
|
164
|
+
...overrides,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse file size string to bytes
|
|
169
|
+
*/
|
|
170
|
+
export function parseFileSize(sizeStr) {
|
|
171
|
+
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
172
|
+
if (!match) {
|
|
173
|
+
throw new Error(`Invalid file size format: ${sizeStr}`);
|
|
174
|
+
}
|
|
175
|
+
const value = parseFloat(match[1]);
|
|
176
|
+
const unit = (match[2] || 'B').toUpperCase();
|
|
177
|
+
const multipliers = {
|
|
178
|
+
B: 1,
|
|
179
|
+
KB: 1024,
|
|
180
|
+
MB: 1024 * 1024,
|
|
181
|
+
GB: 1024 * 1024 * 1024,
|
|
182
|
+
};
|
|
183
|
+
return value * multipliers[unit];
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase credentials management
|
|
3
|
+
* Handles user-level credential storage in ~/.spck-editor/.credentials.json
|
|
4
|
+
*/
|
|
5
|
+
import { StoredCredentials, GlobalConfig } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get the user-level credentials directory
|
|
8
|
+
*/
|
|
9
|
+
export declare function getCredentialsDir(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Get the credentials file path
|
|
12
|
+
*/
|
|
13
|
+
export declare function getCredentialsPath(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get the global config file path
|
|
16
|
+
*/
|
|
17
|
+
export declare function getGlobalConfigPath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Load global config from user-level storage
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadGlobalConfig(): GlobalConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Save global config to user-level storage
|
|
24
|
+
*/
|
|
25
|
+
export declare function saveGlobalConfig(config: GlobalConfig): void;
|
|
26
|
+
/**
|
|
27
|
+
* Get the connection settings file path (project-level)
|
|
28
|
+
* This uses the symlinked project directory
|
|
29
|
+
*/
|
|
30
|
+
export declare function getConnectionSettingsPath(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Load stored credentials from user-level storage
|
|
33
|
+
* Returns only refreshToken + userId; firebaseToken is generated on demand
|
|
34
|
+
* @throws {Error} with code 'CORRUPTED' if file is corrupted
|
|
35
|
+
*/
|
|
36
|
+
export declare function loadCredentials(): StoredCredentials | null;
|
|
37
|
+
/**
|
|
38
|
+
* Save stored credentials to user-level storage
|
|
39
|
+
* Only persists refreshToken + userId (not firebaseToken or expiry)
|
|
40
|
+
* @throws {Error} with code 'EACCES' for permission errors
|
|
41
|
+
* @throws {Error} with code 'ENOSPC' for disk full errors
|
|
42
|
+
*/
|
|
43
|
+
export declare function saveCredentials(credentials: StoredCredentials): void;
|
|
44
|
+
/**
|
|
45
|
+
* Clear credentials (logout)
|
|
46
|
+
*/
|
|
47
|
+
export declare function clearCredentials(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Load connection settings from project-level storage
|
|
50
|
+
* @throws {Error} with code 'CORRUPTED' if file is corrupted
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadConnectionSettings(): any | null;
|
|
53
|
+
/**
|
|
54
|
+
* Save connection settings to project-level storage
|
|
55
|
+
* @throws {Error} with code 'EACCES' for permission errors
|
|
56
|
+
* @throws {Error} with code 'ENOSPC' for disk full errors
|
|
57
|
+
*/
|
|
58
|
+
export declare function saveConnectionSettings(settings: any): void;
|
|
59
|
+
/**
|
|
60
|
+
* Clear connection settings
|
|
61
|
+
*/
|
|
62
|
+
export declare function clearConnectionSettings(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Load saved proxy server preference from user-level credentials
|
|
65
|
+
*/
|
|
66
|
+
export declare function loadServerPreference(): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Save proxy server preference to user-level credentials
|
|
69
|
+
*/
|
|
70
|
+
export declare function saveServerPreference(proxyServerUrl: string): void;
|
|
71
|
+
/**
|
|
72
|
+
* Check if server JWT is expired
|
|
73
|
+
*/
|
|
74
|
+
export declare function isServerTokenExpired(settings: any): boolean;
|
|
75
|
+
//# sourceMappingURL=credentials.d.ts.map
|