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.
Files changed (155) hide show
  1. package/.oxlintrc.json +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +631 -0
  4. package/bin/cli.js +20 -0
  5. package/bin/validate-cwd.js +41 -0
  6. package/dist/config/__tests__/config.test.d.ts +2 -0
  7. package/dist/config/__tests__/config.test.js +262 -0
  8. package/dist/config/__tests__/credentials.test.d.ts +2 -0
  9. package/dist/config/__tests__/credentials.test.js +360 -0
  10. package/dist/config/config.d.ts +33 -0
  11. package/dist/config/config.js +185 -0
  12. package/dist/config/credentials.d.ts +75 -0
  13. package/dist/config/credentials.js +259 -0
  14. package/dist/config/server-selection.d.ts +40 -0
  15. package/dist/config/server-selection.js +130 -0
  16. package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
  17. package/dist/connection/__tests__/firebase-auth.test.js +96 -0
  18. package/dist/connection/__tests__/hmac.test.d.ts +2 -0
  19. package/dist/connection/__tests__/hmac.test.js +372 -0
  20. package/dist/connection/auth.d.ts +13 -0
  21. package/dist/connection/auth.js +91 -0
  22. package/dist/connection/firebase-auth.d.ts +40 -0
  23. package/dist/connection/firebase-auth.js +429 -0
  24. package/dist/connection/hmac.d.ts +24 -0
  25. package/dist/connection/hmac.js +109 -0
  26. package/dist/i18n/index.d.ts +25 -0
  27. package/dist/i18n/index.js +101 -0
  28. package/dist/i18n/locales/en.json +313 -0
  29. package/dist/i18n/locales/es.json +302 -0
  30. package/dist/i18n/locales/fr.json +302 -0
  31. package/dist/i18n/locales/id.json +302 -0
  32. package/dist/i18n/locales/ja.json +302 -0
  33. package/dist/i18n/locales/ko.json +302 -0
  34. package/dist/i18n/locales/locales/en.json +309 -0
  35. package/dist/i18n/locales/locales/es.json +302 -0
  36. package/dist/i18n/locales/locales/fr.json +302 -0
  37. package/dist/i18n/locales/locales/id.json +302 -0
  38. package/dist/i18n/locales/locales/ja.json +302 -0
  39. package/dist/i18n/locales/locales/ko.json +302 -0
  40. package/dist/i18n/locales/locales/pt.json +302 -0
  41. package/dist/i18n/locales/locales/zh-Hans.json +302 -0
  42. package/dist/i18n/locales/pt.json +302 -0
  43. package/dist/i18n/locales/zh-Hans.json +302 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.js +493 -0
  46. package/dist/proxy/ProxyClient.d.ts +125 -0
  47. package/dist/proxy/ProxyClient.js +781 -0
  48. package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
  49. package/dist/proxy/ProxySocketWrapper.js +98 -0
  50. package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
  51. package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
  52. package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
  53. package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
  54. package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
  55. package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
  56. package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
  57. package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
  58. package/dist/proxy/chunking.d.ts +53 -0
  59. package/dist/proxy/chunking.js +127 -0
  60. package/dist/proxy/handshake-validation.d.ts +21 -0
  61. package/dist/proxy/handshake-validation.js +49 -0
  62. package/dist/rpc/__tests__/router.test.d.ts +2 -0
  63. package/dist/rpc/__tests__/router.test.js +262 -0
  64. package/dist/rpc/router.d.ts +37 -0
  65. package/dist/rpc/router.js +132 -0
  66. package/dist/services/BrowserProxyService.d.ts +13 -0
  67. package/dist/services/BrowserProxyService.js +139 -0
  68. package/dist/services/FilesystemService.d.ts +99 -0
  69. package/dist/services/FilesystemService.js +742 -0
  70. package/dist/services/GitService.d.ts +243 -0
  71. package/dist/services/GitService.js +1439 -0
  72. package/dist/services/SearchService.d.ts +93 -0
  73. package/dist/services/SearchService.js +670 -0
  74. package/dist/services/TerminalService.d.ts +62 -0
  75. package/dist/services/TerminalService.js +337 -0
  76. package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
  77. package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
  78. package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
  79. package/dist/services/__tests__/FilesystemService.test.js +609 -0
  80. package/dist/services/__tests__/GitService.test.d.ts +2 -0
  81. package/dist/services/__tests__/GitService.test.js +953 -0
  82. package/dist/services/__tests__/SearchService.test.d.ts +2 -0
  83. package/dist/services/__tests__/SearchService.test.js +384 -0
  84. package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
  85. package/dist/services/__tests__/TerminalService.test.js +513 -0
  86. package/dist/setup/wizard.d.ts +10 -0
  87. package/dist/setup/wizard.js +172 -0
  88. package/dist/types.d.ts +196 -0
  89. package/dist/types.js +44 -0
  90. package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
  91. package/dist/utils/__tests__/gitignore.test.js +127 -0
  92. package/dist/utils/gitignore.d.ts +24 -0
  93. package/dist/utils/gitignore.js +77 -0
  94. package/dist/utils/logger.d.ts +96 -0
  95. package/dist/utils/logger.js +456 -0
  96. package/dist/utils/project-dir.d.ts +51 -0
  97. package/dist/utils/project-dir.js +191 -0
  98. package/dist/utils/ripgrep.d.ts +34 -0
  99. package/dist/utils/ripgrep.js +148 -0
  100. package/dist/utils/tool-detection.d.ts +17 -0
  101. package/dist/utils/tool-detection.js +126 -0
  102. package/dist/watcher/FileWatcher.d.ts +10 -0
  103. package/dist/watcher/FileWatcher.js +42 -0
  104. package/package.json +70 -0
  105. package/src/config/__tests__/config.test.ts +318 -0
  106. package/src/config/__tests__/credentials.test.ts +494 -0
  107. package/src/config/config.ts +206 -0
  108. package/src/config/credentials.ts +302 -0
  109. package/src/config/server-selection.ts +150 -0
  110. package/src/connection/__tests__/firebase-auth.test.ts +121 -0
  111. package/src/connection/__tests__/hmac.test.ts +509 -0
  112. package/src/connection/auth.ts +140 -0
  113. package/src/connection/firebase-auth.ts +504 -0
  114. package/src/connection/hmac.ts +139 -0
  115. package/src/i18n/index.ts +119 -0
  116. package/src/i18n/locales/en.json +313 -0
  117. package/src/i18n/locales/es.json +302 -0
  118. package/src/i18n/locales/fr.json +302 -0
  119. package/src/i18n/locales/id.json +302 -0
  120. package/src/i18n/locales/ja.json +302 -0
  121. package/src/i18n/locales/ko.json +302 -0
  122. package/src/i18n/locales/pt.json +302 -0
  123. package/src/i18n/locales/zh-Hans.json +302 -0
  124. package/src/index.ts +542 -0
  125. package/src/proxy/ProxyClient.ts +968 -0
  126. package/src/proxy/ProxySocketWrapper.ts +113 -0
  127. package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
  128. package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
  129. package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
  130. package/src/proxy/chunking.ts +162 -0
  131. package/src/proxy/handshake-validation.ts +64 -0
  132. package/src/rpc/__tests__/router.test.ts +400 -0
  133. package/src/rpc/router.ts +183 -0
  134. package/src/services/BrowserProxyService.ts +179 -0
  135. package/src/services/FilesystemService.ts +841 -0
  136. package/src/services/GitService.ts +1639 -0
  137. package/src/services/SearchService.ts +809 -0
  138. package/src/services/TerminalService.ts +413 -0
  139. package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
  140. package/src/services/__tests__/FilesystemService.test.ts +1002 -0
  141. package/src/services/__tests__/GitService.test.ts +1552 -0
  142. package/src/services/__tests__/SearchService.test.ts +484 -0
  143. package/src/services/__tests__/TerminalService.test.ts +702 -0
  144. package/src/setup/wizard.ts +242 -0
  145. package/src/types/fossil-delta.d.ts +4 -0
  146. package/src/types.ts +287 -0
  147. package/src/utils/__tests__/gitignore.test.ts +174 -0
  148. package/src/utils/gitignore.ts +91 -0
  149. package/src/utils/logger.ts +578 -0
  150. package/src/utils/project-dir.ts +218 -0
  151. package/src/utils/ripgrep.ts +180 -0
  152. package/src/utils/tool-detection.ts +141 -0
  153. package/src/watcher/FileWatcher.ts +53 -0
  154. package/tsconfig.json +24 -0
  155. 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