forkit-connect 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 (51) hide show
  1. package/QUICKSTART.md +55 -0
  2. package/README.md +96 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +4724 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.js +21 -0
  7. package/dist/launcher.d.ts +33 -0
  8. package/dist/launcher.js +9344 -0
  9. package/dist/ps-list-loader.d.ts +5 -0
  10. package/dist/ps-list-loader.js +20 -0
  11. package/dist/v1/agent-observation.d.ts +42 -0
  12. package/dist/v1/agent-observation.js +499 -0
  13. package/dist/v1/api.d.ts +276 -0
  14. package/dist/v1/api.js +390 -0
  15. package/dist/v1/credential-store.d.ts +92 -0
  16. package/dist/v1/credential-store.js +797 -0
  17. package/dist/v1/currency.d.ts +41 -0
  18. package/dist/v1/currency.js +127 -0
  19. package/dist/v1/daemon.d.ts +50 -0
  20. package/dist/v1/daemon.js +265 -0
  21. package/dist/v1/discovery.d.ts +61 -0
  22. package/dist/v1/discovery.js +168 -0
  23. package/dist/v1/filesystem-models.d.ts +11 -0
  24. package/dist/v1/filesystem-models.js +261 -0
  25. package/dist/v1/heartbeat.d.ts +45 -0
  26. package/dist/v1/heartbeat.js +463 -0
  27. package/dist/v1/lifecycle-monitor.d.ts +78 -0
  28. package/dist/v1/lifecycle-monitor.js +512 -0
  29. package/dist/v1/lmstudio.d.ts +11 -0
  30. package/dist/v1/lmstudio.js +148 -0
  31. package/dist/v1/ollama.d.ts +19 -0
  32. package/dist/v1/ollama.js +164 -0
  33. package/dist/v1/openai-compatible.d.ts +12 -0
  34. package/dist/v1/openai-compatible.js +124 -0
  35. package/dist/v1/process-scout.d.ts +50 -0
  36. package/dist/v1/process-scout.js +715 -0
  37. package/dist/v1/providers.d.ts +50 -0
  38. package/dist/v1/providers.js +106 -0
  39. package/dist/v1/service.d.ts +680 -0
  40. package/dist/v1/service.js +8286 -0
  41. package/dist/v1/state.d.ts +87 -0
  42. package/dist/v1/state.js +1318 -0
  43. package/dist/v1/test-credential-backend.d.ts +19 -0
  44. package/dist/v1/test-credential-backend.js +49 -0
  45. package/dist/v1/types.d.ts +873 -0
  46. package/dist/v1/types.js +3 -0
  47. package/dist/v1/update.d.ts +38 -0
  48. package/dist/v1/update.js +184 -0
  49. package/dist/v1/vitality-pulse.d.ts +36 -0
  50. package/dist/v1/vitality-pulse.js +512 -0
  51. package/package.json +53 -0
@@ -0,0 +1,797 @@
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.ConnectCredentialStore = exports.WindowsDpapiCredentialBackend = exports.ConnectCredentialStoreError = void 0;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ class ConnectCredentialStoreError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'ConnectCredentialStoreError';
17
+ }
18
+ }
19
+ exports.ConnectCredentialStoreError = ConnectCredentialStoreError;
20
+ const SERVICE_BASE_NAME = 'ForkitConnect';
21
+ const LEGACY_SERVICE_NAME = SERVICE_BASE_NAME;
22
+ function nowIso() {
23
+ return new Date().toISOString();
24
+ }
25
+ function sanitizeSecret(value) {
26
+ const normalized = String(value || '').trim();
27
+ return normalized || null;
28
+ }
29
+ function buildCredentialRef(value) {
30
+ return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex').slice(0, 24);
31
+ }
32
+ function buildScopedServiceName(stateDir) {
33
+ const namespace = (0, node_crypto_1.createHash)('sha256')
34
+ .update(node_path_1.default.resolve(stateDir), 'utf8')
35
+ .digest('hex')
36
+ .slice(0, 16);
37
+ return `${SERVICE_BASE_NAME}.v1.${namespace}`;
38
+ }
39
+ function buildEmptyCredentialFileShape() {
40
+ return {
41
+ sessionRef: null,
42
+ runtimeSignalApiKeys: {},
43
+ };
44
+ }
45
+ function normalizedDetail(value) {
46
+ return String(value || '').trim();
47
+ }
48
+ function errnoCode(error) {
49
+ if (!error) {
50
+ return null;
51
+ }
52
+ const candidate = error;
53
+ return typeof candidate.code === 'string' ? candidate.code : null;
54
+ }
55
+ function linuxHeadlessCredentialGuidance() {
56
+ return 'Headless Linux can still authenticate by exporting FORKIT_CONNECT_SESSION_REF, but that path does not persist credentials locally.';
57
+ }
58
+ function windowsHeadlessCredentialGuidance() {
59
+ return 'Headless Windows can still authenticate by exporting FORKIT_CONNECT_SESSION_REF, but that path does not persist credentials locally.';
60
+ }
61
+ function linuxSecretServiceUnavailableDetail(reason) {
62
+ return `${reason} Install libsecret's secret-tool and run Forkit Connect inside a user session with DBUS_SESSION_BUS_ADDRESS set. ${linuxHeadlessCredentialGuidance()}`;
63
+ }
64
+ function windowsDpapiUnavailableDetail(reason) {
65
+ return `${reason} Windows secure credential storage requires powershell.exe and the signed-in user DPAPI profile. ${windowsHeadlessCredentialGuidance()}`;
66
+ }
67
+ function isCredentialNotFound(stderr, status) {
68
+ const normalized = stderr.toLowerCase();
69
+ return status === 44
70
+ || normalized.includes('could not be found')
71
+ || normalized.includes('the specified item could not be found');
72
+ }
73
+ function isLinuxSecretItemNotFound(stderr, status) {
74
+ const normalized = normalizedDetail(stderr).toLowerCase();
75
+ return (status === 1 && !normalized)
76
+ || normalized.includes('no such secret item')
77
+ || normalized.includes('not found');
78
+ }
79
+ function isWindowsCredentialNotFound(stderr, status) {
80
+ const normalized = normalizedDetail(stderr).toLowerCase();
81
+ return status === 44
82
+ || normalized.includes('cannot find path')
83
+ || normalized.includes('could not be found')
84
+ || normalized.includes('not found');
85
+ }
86
+ const WINDOWS_DPAPI_SCRIPTS = {
87
+ status: [
88
+ "$ErrorActionPreference = 'Stop'",
89
+ '[void][System.Security.Cryptography.DataProtectionScope]::CurrentUser',
90
+ "[Console]::Out.Write('windows-dpapi-ready')",
91
+ ].join('; '),
92
+ read: [
93
+ "$ErrorActionPreference = 'Stop'",
94
+ '$path = $env:FORKIT_CONNECT_WINDOWS_CREDENTIAL_PATH',
95
+ 'if (-not (Test-Path -LiteralPath $path)) { exit 44 }',
96
+ '$raw = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)',
97
+ '$trimmed = $raw.Trim()',
98
+ 'if (-not $trimmed) { exit 44 }',
99
+ '$protected = [Convert]::FromBase64String($trimmed)',
100
+ '$entropy = [System.Text.Encoding]::UTF8.GetBytes($env:FORKIT_CONNECT_WINDOWS_CREDENTIAL_ENTROPY)',
101
+ '$plain = [System.Security.Cryptography.ProtectedData]::Unprotect($protected, $entropy, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
102
+ '[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($plain))',
103
+ ].join('; '),
104
+ write: [
105
+ "$ErrorActionPreference = 'Stop'",
106
+ '$path = $env:FORKIT_CONNECT_WINDOWS_CREDENTIAL_PATH',
107
+ '$directory = Split-Path -Parent $path',
108
+ 'if ($directory) { [System.IO.Directory]::CreateDirectory($directory) | Out-Null }',
109
+ '$inputText = [Console]::In.ReadToEnd()',
110
+ '$plainBytes = [System.Text.Encoding]::UTF8.GetBytes($inputText)',
111
+ '$entropy = [System.Text.Encoding]::UTF8.GetBytes($env:FORKIT_CONNECT_WINDOWS_CREDENTIAL_ENTROPY)',
112
+ '$protected = [System.Security.Cryptography.ProtectedData]::Protect($plainBytes, $entropy, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
113
+ '$encoded = [Convert]::ToBase64String($protected)',
114
+ '[System.IO.File]::WriteAllText($path, $encoded, [System.Text.Encoding]::UTF8)',
115
+ ].join('; '),
116
+ delete: [
117
+ "$ErrorActionPreference = 'Stop'",
118
+ '$path = $env:FORKIT_CONNECT_WINDOWS_CREDENTIAL_PATH',
119
+ 'if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force }',
120
+ ].join('; '),
121
+ };
122
+ function normalizeWindowsDpapiCommandResult(result) {
123
+ return {
124
+ status: result.status ?? null,
125
+ stdout: String(result.stdout || ''),
126
+ stderr: String(result.stderr || ''),
127
+ error: result.error ?? null,
128
+ };
129
+ }
130
+ function runWindowsDpapiPowerShell(operation, options) {
131
+ const result = (0, node_child_process_1.spawnSync)('powershell.exe', [
132
+ '-NoProfile',
133
+ '-NonInteractive',
134
+ '-Command',
135
+ WINDOWS_DPAPI_SCRIPTS[operation],
136
+ ], {
137
+ encoding: 'utf8',
138
+ input: options.input,
139
+ env: {
140
+ ...process.env,
141
+ FORKIT_CONNECT_WINDOWS_CREDENTIAL_PATH: options.credentialPath,
142
+ FORKIT_CONNECT_WINDOWS_CREDENTIAL_ENTROPY: options.entropy,
143
+ },
144
+ });
145
+ return normalizeWindowsDpapiCommandResult(result);
146
+ }
147
+ class UnsupportedSecureCredentialBackend {
148
+ detail;
149
+ backend = 'unsupported';
150
+ constructor(detail) {
151
+ this.detail = detail;
152
+ }
153
+ read() {
154
+ return null;
155
+ }
156
+ write() {
157
+ throw new ConnectCredentialStoreError('secure_storage_unavailable', this.detail);
158
+ }
159
+ delete() {
160
+ throw new ConnectCredentialStoreError('secure_storage_unavailable', this.detail);
161
+ }
162
+ getStatus() {
163
+ return {
164
+ backend: this.backend,
165
+ available: false,
166
+ detail: this.detail,
167
+ };
168
+ }
169
+ }
170
+ class MacOsKeychainBackend {
171
+ serviceName;
172
+ backend = 'macos-keychain';
173
+ constructor(serviceName) {
174
+ this.serviceName = serviceName;
175
+ }
176
+ read(account) {
177
+ const result = (0, node_child_process_1.spawnSync)('security', [
178
+ 'find-generic-password',
179
+ '-s',
180
+ this.serviceName,
181
+ '-a',
182
+ account,
183
+ '-w',
184
+ ], {
185
+ encoding: 'utf8',
186
+ });
187
+ if (result.error) {
188
+ throw new ConnectCredentialStoreError('secure_storage_read_failed', `Forkit Connect could not read secure credentials from macOS Keychain: ${result.error.message}`);
189
+ }
190
+ if (result.status === 0) {
191
+ return String(result.stdout || '').trim() || null;
192
+ }
193
+ if (isCredentialNotFound(String(result.stderr || ''), result.status)) {
194
+ return null;
195
+ }
196
+ throw new ConnectCredentialStoreError('secure_storage_read_failed', `Forkit Connect could not read secure credentials from macOS Keychain (${String(result.stderr || '').trim() || `status ${result.status}`}).`);
197
+ }
198
+ write(account, value) {
199
+ const result = (0, node_child_process_1.spawnSync)('security', [
200
+ 'add-generic-password',
201
+ '-U',
202
+ '-s',
203
+ this.serviceName,
204
+ '-a',
205
+ account,
206
+ '-w',
207
+ value,
208
+ ], {
209
+ encoding: 'utf8',
210
+ });
211
+ if (result.error) {
212
+ throw new ConnectCredentialStoreError('secure_storage_write_failed', `Forkit Connect could not write secure credentials to macOS Keychain: ${result.error.message}`);
213
+ }
214
+ if (result.status === 0) {
215
+ return;
216
+ }
217
+ throw new ConnectCredentialStoreError('secure_storage_write_failed', `Forkit Connect could not write secure credentials to macOS Keychain (${String(result.stderr || '').trim() || `status ${result.status}`}).`);
218
+ }
219
+ delete(account) {
220
+ const result = (0, node_child_process_1.spawnSync)('security', [
221
+ 'delete-generic-password',
222
+ '-s',
223
+ this.serviceName,
224
+ '-a',
225
+ account,
226
+ ], {
227
+ encoding: 'utf8',
228
+ });
229
+ if (result.error) {
230
+ throw new ConnectCredentialStoreError('secure_storage_delete_failed', `Forkit Connect could not remove secure credentials from macOS Keychain: ${result.error.message}`);
231
+ }
232
+ if (result.status === 0 || isCredentialNotFound(String(result.stderr || ''), result.status)) {
233
+ return;
234
+ }
235
+ throw new ConnectCredentialStoreError('secure_storage_delete_failed', `Forkit Connect could not remove secure credentials from macOS Keychain (${String(result.stderr || '').trim() || `status ${result.status}`}).`);
236
+ }
237
+ getStatus() {
238
+ const result = (0, node_child_process_1.spawnSync)('security', ['list-keychains'], {
239
+ encoding: 'utf8',
240
+ });
241
+ if (result.error) {
242
+ return {
243
+ backend: this.backend,
244
+ available: false,
245
+ detail: `macOS Keychain command failed: ${result.error.message}`,
246
+ };
247
+ }
248
+ if (result.status === 0) {
249
+ return {
250
+ backend: this.backend,
251
+ available: true,
252
+ detail: `macOS Keychain available for ${this.serviceName}.`,
253
+ };
254
+ }
255
+ return {
256
+ backend: this.backend,
257
+ available: false,
258
+ detail: `macOS Keychain unavailable (${String(result.stderr || '').trim() || `status ${result.status}`}).`,
259
+ };
260
+ }
261
+ }
262
+ class LinuxSecretServiceBackend {
263
+ serviceName;
264
+ backend = 'linux-secret-service';
265
+ constructor(serviceName) {
266
+ this.serviceName = serviceName;
267
+ }
268
+ baseArgs(account) {
269
+ return ['service', this.serviceName, 'account', account];
270
+ }
271
+ read(account) {
272
+ const result = (0, node_child_process_1.spawnSync)('secret-tool', [
273
+ 'lookup',
274
+ ...this.baseArgs(account),
275
+ ], {
276
+ encoding: 'utf8',
277
+ });
278
+ if (result.error) {
279
+ const detail = errnoCode(result.error) === 'ENOENT'
280
+ ? linuxSecretServiceUnavailableDetail('Linux Secret Service CLI secret-tool is unavailable.')
281
+ : `Forkit Connect could not read secure credentials from Linux Secret Service: ${result.error.message}`;
282
+ throw new ConnectCredentialStoreError('secure_storage_read_failed', detail);
283
+ }
284
+ if (result.status === 0) {
285
+ return normalizedDetail(result.stdout) || null;
286
+ }
287
+ if (isLinuxSecretItemNotFound(String(result.stderr || ''), result.status)) {
288
+ return null;
289
+ }
290
+ throw new ConnectCredentialStoreError('secure_storage_read_failed', `Forkit Connect could not read secure credentials from Linux Secret Service (${normalizedDetail(result.stderr) || `status ${result.status}`}).`);
291
+ }
292
+ write(account, value) {
293
+ const result = (0, node_child_process_1.spawnSync)('secret-tool', [
294
+ 'store',
295
+ '--label',
296
+ `Forkit Connect ${account}`,
297
+ ...this.baseArgs(account),
298
+ ], {
299
+ encoding: 'utf8',
300
+ input: `${value}\n`,
301
+ });
302
+ if (result.error) {
303
+ const detail = errnoCode(result.error) === 'ENOENT'
304
+ ? linuxSecretServiceUnavailableDetail('Linux Secret Service CLI secret-tool is unavailable.')
305
+ : `Forkit Connect could not write secure credentials to Linux Secret Service: ${result.error.message}`;
306
+ throw new ConnectCredentialStoreError('secure_storage_write_failed', detail);
307
+ }
308
+ if (result.status === 0) {
309
+ return;
310
+ }
311
+ throw new ConnectCredentialStoreError('secure_storage_write_failed', `Forkit Connect could not write secure credentials to Linux Secret Service (${normalizedDetail(result.stderr) || `status ${result.status}`}).`);
312
+ }
313
+ delete(account) {
314
+ const result = (0, node_child_process_1.spawnSync)('secret-tool', [
315
+ 'clear',
316
+ ...this.baseArgs(account),
317
+ ], {
318
+ encoding: 'utf8',
319
+ });
320
+ if (result.error) {
321
+ const detail = errnoCode(result.error) === 'ENOENT'
322
+ ? linuxSecretServiceUnavailableDetail('Linux Secret Service CLI secret-tool is unavailable.')
323
+ : `Forkit Connect could not remove secure credentials from Linux Secret Service: ${result.error.message}`;
324
+ throw new ConnectCredentialStoreError('secure_storage_delete_failed', detail);
325
+ }
326
+ if (result.status === 0 || isLinuxSecretItemNotFound(String(result.stderr || ''), result.status)) {
327
+ return;
328
+ }
329
+ throw new ConnectCredentialStoreError('secure_storage_delete_failed', `Forkit Connect could not remove secure credentials from Linux Secret Service (${normalizedDetail(result.stderr) || `status ${result.status}`}).`);
330
+ }
331
+ getStatus() {
332
+ if (!normalizedDetail(process.env.DBUS_SESSION_BUS_ADDRESS)) {
333
+ return {
334
+ backend: this.backend,
335
+ available: false,
336
+ detail: linuxSecretServiceUnavailableDetail('Linux Secret Service session bus is unavailable.'),
337
+ };
338
+ }
339
+ const result = (0, node_child_process_1.spawnSync)('secret-tool', ['search', 'service', this.serviceName], {
340
+ encoding: 'utf8',
341
+ });
342
+ if (result.error) {
343
+ return {
344
+ backend: this.backend,
345
+ available: false,
346
+ detail: errnoCode(result.error) === 'ENOENT'
347
+ ? linuxSecretServiceUnavailableDetail('Linux Secret Service CLI secret-tool is unavailable.')
348
+ : `Linux Secret Service command failed: ${result.error.message}`,
349
+ };
350
+ }
351
+ const stderr = normalizedDetail(result.stderr);
352
+ if (result.status === 0 || (result.status === 1 && !stderr)) {
353
+ return {
354
+ backend: this.backend,
355
+ available: true,
356
+ detail: `Linux Secret Service available for ${this.serviceName}.`,
357
+ };
358
+ }
359
+ return {
360
+ backend: this.backend,
361
+ available: false,
362
+ detail: linuxSecretServiceUnavailableDetail(`Linux Secret Service unavailable (${stderr || `status ${result.status}`}).`),
363
+ };
364
+ }
365
+ }
366
+ class WindowsDpapiCredentialBackend {
367
+ serviceName;
368
+ stateDir;
369
+ runner;
370
+ backend = 'windows-dpapi';
371
+ constructor(serviceName, stateDir, runner = runWindowsDpapiPowerShell) {
372
+ this.serviceName = serviceName;
373
+ this.stateDir = stateDir;
374
+ this.runner = runner;
375
+ }
376
+ credentialPath(account) {
377
+ const accountHash = (0, node_crypto_1.createHash)('sha256')
378
+ .update(`${this.serviceName}|${account}`, 'utf8')
379
+ .digest('hex');
380
+ return node_path_1.default.join(this.stateDir, 'secure-credentials-win32', `${accountHash}.dpapi`);
381
+ }
382
+ entropy(account) {
383
+ return `${this.serviceName}|${account}`;
384
+ }
385
+ read(account) {
386
+ const result = this.runner('read', {
387
+ credentialPath: this.credentialPath(account),
388
+ entropy: this.entropy(account),
389
+ });
390
+ if (result.error) {
391
+ const detail = errnoCode(result.error) === 'ENOENT'
392
+ ? windowsDpapiUnavailableDetail('Windows PowerShell is unavailable for DPAPI secure storage.')
393
+ : `Forkit Connect could not read secure credentials from Windows DPAPI: ${result.error.message}`;
394
+ throw new ConnectCredentialStoreError('secure_storage_read_failed', detail);
395
+ }
396
+ if (result.status === 0) {
397
+ return normalizedDetail(result.stdout) || null;
398
+ }
399
+ if (isWindowsCredentialNotFound(result.stderr, result.status)) {
400
+ return null;
401
+ }
402
+ throw new ConnectCredentialStoreError('secure_storage_read_failed', `Forkit Connect could not read secure credentials from Windows DPAPI (${normalizedDetail(result.stderr) || `status ${result.status}`}).`);
403
+ }
404
+ write(account, value) {
405
+ const result = this.runner('write', {
406
+ credentialPath: this.credentialPath(account),
407
+ entropy: this.entropy(account),
408
+ input: value,
409
+ });
410
+ if (result.error) {
411
+ const detail = errnoCode(result.error) === 'ENOENT'
412
+ ? windowsDpapiUnavailableDetail('Windows PowerShell is unavailable for DPAPI secure storage.')
413
+ : `Forkit Connect could not write secure credentials to Windows DPAPI: ${result.error.message}`;
414
+ throw new ConnectCredentialStoreError('secure_storage_write_failed', detail);
415
+ }
416
+ if (result.status === 0) {
417
+ return;
418
+ }
419
+ throw new ConnectCredentialStoreError('secure_storage_write_failed', `Forkit Connect could not write secure credentials to Windows DPAPI (${normalizedDetail(result.stderr) || `status ${result.status}`}).`);
420
+ }
421
+ delete(account) {
422
+ const result = this.runner('delete', {
423
+ credentialPath: this.credentialPath(account),
424
+ entropy: this.entropy(account),
425
+ });
426
+ if (result.error) {
427
+ const detail = errnoCode(result.error) === 'ENOENT'
428
+ ? windowsDpapiUnavailableDetail('Windows PowerShell is unavailable for DPAPI secure storage.')
429
+ : `Forkit Connect could not remove secure credentials from Windows DPAPI: ${result.error.message}`;
430
+ throw new ConnectCredentialStoreError('secure_storage_delete_failed', detail);
431
+ }
432
+ if (result.status === 0 || isWindowsCredentialNotFound(result.stderr, result.status)) {
433
+ return;
434
+ }
435
+ throw new ConnectCredentialStoreError('secure_storage_delete_failed', `Forkit Connect could not remove secure credentials from Windows DPAPI (${normalizedDetail(result.stderr) || `status ${result.status}`}).`);
436
+ }
437
+ getStatus() {
438
+ const result = this.runner('status', {
439
+ credentialPath: node_path_1.default.join(this.stateDir, 'secure-credentials-win32', '.probe'),
440
+ entropy: this.serviceName,
441
+ });
442
+ if (result.error) {
443
+ return {
444
+ backend: this.backend,
445
+ available: false,
446
+ detail: errnoCode(result.error) === 'ENOENT'
447
+ ? windowsDpapiUnavailableDetail('Windows PowerShell is unavailable for DPAPI secure storage.')
448
+ : `Windows DPAPI command failed: ${result.error.message}`,
449
+ };
450
+ }
451
+ if (result.status === 0) {
452
+ return {
453
+ backend: this.backend,
454
+ available: true,
455
+ detail: `Windows DPAPI secure storage available for ${this.serviceName}.`,
456
+ };
457
+ }
458
+ return {
459
+ backend: this.backend,
460
+ available: false,
461
+ detail: windowsDpapiUnavailableDetail(`Windows DPAPI unavailable (${normalizedDetail(result.stderr) || `status ${result.status}`}).`),
462
+ };
463
+ }
464
+ }
465
+ exports.WindowsDpapiCredentialBackend = WindowsDpapiCredentialBackend;
466
+ function createDefaultSecureCredentialBackend(stateDir) {
467
+ if (process.platform === 'darwin') {
468
+ return new MacOsKeychainBackend(buildScopedServiceName(stateDir));
469
+ }
470
+ if (process.platform === 'linux') {
471
+ return new LinuxSecretServiceBackend(buildScopedServiceName(stateDir));
472
+ }
473
+ if (process.platform === 'win32') {
474
+ return new WindowsDpapiCredentialBackend(buildScopedServiceName(stateDir), stateDir);
475
+ }
476
+ return new UnsupportedSecureCredentialBackend('Forkit Connect V1 secure credential storage is currently supported on macOS, Linux Secret Service, and Windows DPAPI.');
477
+ }
478
+ function createLegacySecureCredentialBackend() {
479
+ if (process.platform === 'darwin') {
480
+ return new MacOsKeychainBackend(LEGACY_SERVICE_NAME);
481
+ }
482
+ return null;
483
+ }
484
+ class ConnectCredentialStore {
485
+ stateDir;
486
+ sessionAccount = 'connect-session-ref';
487
+ credentialFile;
488
+ backend;
489
+ legacyBackend;
490
+ allowPlaintextFallback;
491
+ cache = new Map();
492
+ lastBackendError = null;
493
+ constructor(stateDir, backend, legacyBackend) {
494
+ this.stateDir = stateDir;
495
+ this.credentialFile = node_path_1.default.join(stateDir, 'credentials.v1.json');
496
+ this.backend = backend ?? createDefaultSecureCredentialBackend(stateDir);
497
+ this.legacyBackend = legacyBackend === undefined ? createLegacySecureCredentialBackend() : legacyBackend;
498
+ this.allowPlaintextFallback = process.env.FORKIT_CONNECT_ALLOW_PLAINTEXT_FALLBACK !== '0';
499
+ }
500
+ getCredentialFilePath() {
501
+ return this.credentialFile;
502
+ }
503
+ getStatus() {
504
+ const backendStatus = this.backend.getStatus();
505
+ const legacyPlaintextFilePresent = node_fs_1.default.existsSync(this.credentialFile);
506
+ const fallbackActive = this.allowPlaintextFallback && legacyPlaintextFilePresent;
507
+ const detail = fallbackActive
508
+ ? `${this.lastBackendError?.message ?? backendStatus.detail} Connect is using local plaintext credential fallback at ${this.credentialFile}.`
509
+ : this.lastBackendError?.message ?? backendStatus.detail;
510
+ return {
511
+ backend: backendStatus.backend,
512
+ available: fallbackActive || (backendStatus.available && !this.lastBackendError),
513
+ detail,
514
+ legacyPlaintextFilePresent,
515
+ };
516
+ }
517
+ migrateLegacyPlaintextFile() {
518
+ if (!node_fs_1.default.existsSync(this.credentialFile)) {
519
+ return;
520
+ }
521
+ const legacy = this.readLegacyFile();
522
+ const sessionRef = sanitizeSecret(legacy.sessionRef?.value);
523
+ const runtimeSignalEntries = Object.entries(legacy.runtimeSignalApiKeys ?? {});
524
+ let usedFallbackDuringMigration = false;
525
+ this.lastBackendError = null;
526
+ try {
527
+ if (sessionRef) {
528
+ this.writeAccount(this.sessionAccount, sessionRef);
529
+ if (this.allowPlaintextFallback && this.lastBackendError) {
530
+ usedFallbackDuringMigration = true;
531
+ }
532
+ }
533
+ for (const [gaid, record] of runtimeSignalEntries) {
534
+ const normalizedKey = sanitizeSecret(record?.value);
535
+ if (!normalizedKey)
536
+ continue;
537
+ const normalizedGaid = sanitizeSecret(gaid);
538
+ if (!normalizedGaid)
539
+ continue;
540
+ this.writeAccount(this.runtimeSignalAccount(normalizedGaid), normalizedKey);
541
+ if (this.allowPlaintextFallback && this.lastBackendError) {
542
+ usedFallbackDuringMigration = true;
543
+ }
544
+ }
545
+ }
546
+ catch (error) {
547
+ if (error instanceof ConnectCredentialStoreError) {
548
+ this.lastBackendError = new ConnectCredentialStoreError(error.code === 'secure_storage_write_failed' ? 'legacy_plaintext_migration_failed' : error.code, `Forkit Connect could not migrate existing plaintext credentials into secure storage. ${error.message}`);
549
+ return;
550
+ }
551
+ throw error;
552
+ }
553
+ if (usedFallbackDuringMigration) {
554
+ return;
555
+ }
556
+ node_fs_1.default.rmSync(this.credentialFile, { force: true });
557
+ }
558
+ getSessionRef() {
559
+ return this.readAccount(this.sessionAccount);
560
+ }
561
+ setSessionRef(sessionRef) {
562
+ const value = sanitizeSecret(sessionRef);
563
+ if (!value) {
564
+ return this.clearSessionRef();
565
+ }
566
+ const updatedAt = nowIso();
567
+ const credentialRef = buildCredentialRef(value);
568
+ this.writeAccount(this.sessionAccount, value);
569
+ return { credentialRef, updatedAt };
570
+ }
571
+ clearSessionRef() {
572
+ const updatedAt = nowIso();
573
+ this.deleteAccount(this.sessionAccount);
574
+ return { credentialRef: null, updatedAt };
575
+ }
576
+ getRuntimeSignalApiKey(gaid) {
577
+ const normalizedGaid = sanitizeSecret(gaid);
578
+ if (!normalizedGaid)
579
+ return null;
580
+ return this.readAccount(this.runtimeSignalAccount(normalizedGaid));
581
+ }
582
+ setRuntimeSignalApiKey(gaid, apiKey) {
583
+ const normalizedGaid = sanitizeSecret(gaid);
584
+ const normalizedKey = sanitizeSecret(apiKey);
585
+ if (!normalizedGaid || !normalizedKey) {
586
+ return this.clearRuntimeSignalApiKey(gaid);
587
+ }
588
+ const updatedAt = nowIso();
589
+ const credentialRef = buildCredentialRef(normalizedKey);
590
+ this.writeAccount(this.runtimeSignalAccount(normalizedGaid), normalizedKey);
591
+ return { credentialRef, updatedAt };
592
+ }
593
+ clearRuntimeSignalApiKey(gaid) {
594
+ const normalizedGaid = sanitizeSecret(gaid);
595
+ const updatedAt = nowIso();
596
+ if (!normalizedGaid) {
597
+ return { credentialRef: null, updatedAt };
598
+ }
599
+ this.deleteAccount(this.runtimeSignalAccount(normalizedGaid));
600
+ return { credentialRef: null, updatedAt };
601
+ }
602
+ runtimeSignalAccount(gaid) {
603
+ return `runtimeSignal:${gaid}`;
604
+ }
605
+ readAccount(account) {
606
+ try {
607
+ const current = this.backend.read(account);
608
+ if (current) {
609
+ this.cache.set(account, current);
610
+ this.lastBackendError = null;
611
+ return current;
612
+ }
613
+ }
614
+ catch (error) {
615
+ if (error instanceof ConnectCredentialStoreError) {
616
+ this.lastBackendError = error;
617
+ }
618
+ else {
619
+ this.lastBackendError = new ConnectCredentialStoreError('secure_storage_read_failed', error instanceof Error
620
+ ? error.message
621
+ : 'Forkit Connect could not read secure credentials.');
622
+ }
623
+ }
624
+ const legacy = this.readLegacyAccount(account);
625
+ if (legacy) {
626
+ try {
627
+ this.writeAccount(account, legacy);
628
+ }
629
+ catch {
630
+ this.cache.set(account, legacy);
631
+ }
632
+ return legacy;
633
+ }
634
+ this.cache.delete(account);
635
+ return null;
636
+ }
637
+ writeAccount(account, value) {
638
+ try {
639
+ this.backend.write(account, value);
640
+ }
641
+ catch (error) {
642
+ if (error instanceof ConnectCredentialStoreError) {
643
+ this.lastBackendError = error;
644
+ if (this.allowPlaintextFallback && (error.code === 'secure_storage_unavailable' || error.code === 'secure_storage_write_failed')) {
645
+ this.writeLegacyAccount(account, value);
646
+ this.cache.set(account, value);
647
+ return;
648
+ }
649
+ throw error;
650
+ }
651
+ const wrapped = new ConnectCredentialStoreError('secure_storage_write_failed', error instanceof Error
652
+ ? error.message
653
+ : 'Forkit Connect could not write secure credentials.');
654
+ this.lastBackendError = wrapped;
655
+ if (this.allowPlaintextFallback) {
656
+ this.writeLegacyAccount(account, value);
657
+ this.cache.set(account, value);
658
+ return;
659
+ }
660
+ throw wrapped;
661
+ }
662
+ this.lastBackendError = null;
663
+ this.cache.set(account, value);
664
+ this.deleteLegacyAccount(account);
665
+ }
666
+ deleteAccount(account) {
667
+ try {
668
+ this.backend.delete(account);
669
+ }
670
+ catch (error) {
671
+ if (error instanceof ConnectCredentialStoreError) {
672
+ this.lastBackendError = error;
673
+ if (this.allowPlaintextFallback && (error.code === 'secure_storage_unavailable' || error.code === 'secure_storage_delete_failed')) {
674
+ this.deleteLegacyAccount(account);
675
+ this.cache.set(account, null);
676
+ return;
677
+ }
678
+ throw error;
679
+ }
680
+ const wrapped = new ConnectCredentialStoreError('secure_storage_delete_failed', error instanceof Error
681
+ ? error.message
682
+ : 'Forkit Connect could not remove secure credentials.');
683
+ this.lastBackendError = wrapped;
684
+ if (this.allowPlaintextFallback) {
685
+ this.deleteLegacyAccount(account);
686
+ this.cache.set(account, null);
687
+ return;
688
+ }
689
+ throw wrapped;
690
+ }
691
+ this.lastBackendError = null;
692
+ this.cache.set(account, null);
693
+ this.deleteLegacyAccount(account);
694
+ }
695
+ writeLegacyAccount(account, value) {
696
+ const updatedAt = nowIso();
697
+ const credentialRef = buildCredentialRef(value);
698
+ const legacy = this.readLegacyFile();
699
+ if (account === this.sessionAccount) {
700
+ legacy.sessionRef = {
701
+ value,
702
+ ref: credentialRef,
703
+ updatedAt,
704
+ };
705
+ this.writeLegacyFile(legacy);
706
+ return;
707
+ }
708
+ if (account.startsWith('runtimeSignal:')) {
709
+ const gaid = sanitizeSecret(account.slice('runtimeSignal:'.length));
710
+ if (!gaid)
711
+ return;
712
+ legacy.runtimeSignalApiKeys = legacy.runtimeSignalApiKeys ?? {};
713
+ legacy.runtimeSignalApiKeys[gaid] = {
714
+ value,
715
+ ref: credentialRef,
716
+ updatedAt,
717
+ };
718
+ this.writeLegacyFile(legacy);
719
+ }
720
+ }
721
+ readLegacyAccount(account) {
722
+ if (account === this.sessionAccount) {
723
+ return sanitizeSecret(this.readLegacyFile().sessionRef?.value);
724
+ }
725
+ if (account.startsWith('runtimeSignal:')) {
726
+ const gaid = sanitizeSecret(account.slice('runtimeSignal:'.length));
727
+ if (!gaid)
728
+ return null;
729
+ return sanitizeSecret(this.readLegacyFile().runtimeSignalApiKeys?.[gaid]?.value);
730
+ }
731
+ if (!this.legacyBackend)
732
+ return null;
733
+ try {
734
+ return this.legacyBackend.read(account);
735
+ }
736
+ catch {
737
+ return null;
738
+ }
739
+ }
740
+ deleteLegacyAccount(account) {
741
+ const legacy = this.readLegacyFile();
742
+ let updatedLegacyFile = false;
743
+ if (account === this.sessionAccount && legacy.sessionRef) {
744
+ legacy.sessionRef = null;
745
+ updatedLegacyFile = true;
746
+ }
747
+ if (account.startsWith('runtimeSignal:')) {
748
+ const gaid = sanitizeSecret(account.slice('runtimeSignal:'.length));
749
+ if (gaid && legacy.runtimeSignalApiKeys?.[gaid]) {
750
+ delete legacy.runtimeSignalApiKeys[gaid];
751
+ updatedLegacyFile = true;
752
+ }
753
+ }
754
+ if (updatedLegacyFile) {
755
+ this.writeLegacyFile(legacy);
756
+ }
757
+ if (!this.legacyBackend)
758
+ return;
759
+ try {
760
+ this.legacyBackend.delete(account);
761
+ }
762
+ catch {
763
+ return;
764
+ }
765
+ }
766
+ readLegacyFile() {
767
+ if (!node_fs_1.default.existsSync(this.credentialFile)) {
768
+ return buildEmptyCredentialFileShape();
769
+ }
770
+ try {
771
+ const raw = node_fs_1.default.readFileSync(this.credentialFile, 'utf8');
772
+ const parsed = JSON.parse(raw);
773
+ return {
774
+ sessionRef: parsed.sessionRef ?? null,
775
+ runtimeSignalApiKeys: parsed.runtimeSignalApiKeys ?? {},
776
+ };
777
+ }
778
+ catch {
779
+ return buildEmptyCredentialFileShape();
780
+ }
781
+ }
782
+ writeLegacyFile(shape) {
783
+ const normalized = {
784
+ sessionRef: shape.sessionRef ?? null,
785
+ runtimeSignalApiKeys: shape.runtimeSignalApiKeys ?? {},
786
+ };
787
+ const hasSessionRef = Boolean(normalized.sessionRef?.value);
788
+ const hasRuntimeSignalKeys = Object.keys(normalized.runtimeSignalApiKeys || {}).length > 0;
789
+ if (!hasSessionRef && !hasRuntimeSignalKeys) {
790
+ node_fs_1.default.rmSync(this.credentialFile, { force: true });
791
+ return;
792
+ }
793
+ node_fs_1.default.writeFileSync(this.credentialFile, JSON.stringify(normalized, null, 2));
794
+ }
795
+ }
796
+ exports.ConnectCredentialStore = ConnectCredentialStore;
797
+ //# sourceMappingURL=credential-store.js.map