latchkey 2.7.3 → 2.9.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.
- package/README.md +55 -5
- package/dist/scripts/cryptFile.js +2 -2
- package/dist/scripts/cryptFile.js.map +1 -1
- package/dist/scripts/recordBrowserSession.js +3 -2
- package/dist/scripts/recordBrowserSession.js.map +1 -1
- package/dist/src/cli.js +5 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts +1 -1
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +44 -6
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +34 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +53 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/curlInjection.d.ts +1 -1
- package/dist/src/curlInjection.d.ts.map +1 -1
- package/dist/src/curlInjection.js +16 -1
- package/dist/src/curlInjection.js.map +1 -1
- package/dist/src/encryptedStorage.d.ts +9 -25
- package/dist/src/encryptedStorage.d.ts.map +1 -1
- package/dist/src/encryptedStorage.js +9 -52
- package/dist/src/encryptedStorage.js.map +1 -1
- package/dist/src/encryption.d.ts +45 -0
- package/dist/src/encryption.d.ts.map +1 -1
- package/dist/src/encryption.js +69 -0
- package/dist/src/encryption.js.map +1 -1
- package/dist/src/gateway/client.d.ts +12 -2
- package/dist/src/gateway/client.d.ts.map +1 -1
- package/dist/src/gateway/client.js +31 -4
- package/dist/src/gateway/client.js.map +1 -1
- package/dist/src/gateway/extensions.d.ts +59 -0
- package/dist/src/gateway/extensions.d.ts.map +1 -0
- package/dist/src/gateway/extensions.js +170 -0
- package/dist/src/gateway/extensions.js.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts +22 -1
- package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
- package/dist/src/gateway/gatewayEndpoint.js +52 -15
- package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
- package/dist/src/gateway/password.d.ts +16 -0
- package/dist/src/gateway/password.d.ts.map +1 -0
- package/dist/src/gateway/password.js +24 -0
- package/dist/src/gateway/password.js.map +1 -0
- package/dist/src/gateway/permissionsOverride.d.ts +65 -0
- package/dist/src/gateway/permissionsOverride.d.ts.map +1 -0
- package/dist/src/gateway/permissionsOverride.js +171 -0
- package/dist/src/gateway/permissionsOverride.js.map +1 -0
- package/dist/src/gateway/server.d.ts.map +1 -1
- package/dist/src/gateway/server.js +100 -15
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauthUtils.d.ts +11 -2
- package/dist/src/oauthUtils.d.ts.map +1 -1
- package/dist/src/oauthUtils.js +25 -4
- package/dist/src/oauthUtils.js.map +1 -1
- package/dist/src/permissions.d.ts +3 -6
- package/dist/src/permissions.d.ts.map +1 -1
- package/dist/src/permissions.js +6 -13
- package/dist/src/permissions.js.map +1 -1
- package/dist/src/serviceRegistry.d.ts.map +1 -1
- package/dist/src/serviceRegistry.js +2 -1
- package/dist/src/serviceRegistry.js.map +1 -1
- package/dist/src/services/index.d.ts +1 -0
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/notion-mcp.d.ts +29 -0
- package/dist/src/services/notion-mcp.d.ts.map +1 -0
- package/dist/src/services/notion-mcp.js +156 -0
- package/dist/src/services/notion-mcp.js.map +1 -0
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +3 -2
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/tests/apiCredentialStore.test.js +2 -2
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/cli.test.js +98 -53
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/config.test.js +37 -0
- package/dist/tests/config.test.js.map +1 -1
- package/dist/tests/encryptedStorage.test.js +19 -39
- package/dist/tests/encryptedStorage.test.js.map +1 -1
- package/dist/tests/gateway.test.js +184 -7
- package/dist/tests/gateway.test.js.map +1 -1
- package/dist/tests/gatewayClient.test.js +74 -0
- package/dist/tests/gatewayClient.test.js.map +1 -1
- package/dist/tests/gatewayExtensions.test.d.ts +2 -0
- package/dist/tests/gatewayExtensions.test.d.ts.map +1 -0
- package/dist/tests/gatewayExtensions.test.js +398 -0
- package/dist/tests/gatewayExtensions.test.js.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.js +7 -6
- package/dist/tests/latchkeyEndpoint.test.js.map +1 -1
- package/dist/tests/migrations.test.js +2 -2
- package/dist/tests/migrations.test.js.map +1 -1
- package/dist/tests/oauthUtils.test.d.ts +2 -0
- package/dist/tests/oauthUtils.test.d.ts.map +1 -0
- package/dist/tests/oauthUtils.test.js +63 -0
- package/dist/tests/oauthUtils.test.js.map +1 -0
- package/dist/tests/permissions.test.js +14 -10
- package/dist/tests/permissions.test.js.map +1 -1
- package/dist/tests/permissionsOverride.test.d.ts +2 -0
- package/dist/tests/permissionsOverride.test.d.ts.map +1 -0
- package/dist/tests/permissionsOverride.test.js +136 -0
- package/dist/tests/permissionsOverride.test.js.map +1 -0
- package/dist/tests/resolveEncryptionKey.test.d.ts +2 -0
- package/dist/tests/resolveEncryptionKey.test.d.ts.map +1 -0
- package/dist/tests/resolveEncryptionKey.test.js +26 -0
- package/dist/tests/resolveEncryptionKey.test.js.map +1 -0
- package/dist/tests/sharedOperations.test.js +34 -50
- package/dist/tests/sharedOperations.test.js.map +1 -1
- package/package.json +2 -2
- package/dist/tests/encryptedStorageKeyGeneration.test.d.ts +0 -2
- package/dist/tests/encryptedStorageKeyGeneration.test.d.ts.map +0 -1
- package/dist/tests/encryptedStorageKeyGeneration.test.js +0 -22
- package/dist/tests/encryptedStorageKeyGeneration.test.js.map +0 -1
package/dist/tests/cli.test.js
CHANGED
|
@@ -16,17 +16,18 @@ import { NoCurlCredentialsNotSupportedError, Service } from '../src/services/cor
|
|
|
16
16
|
import { RegisteredService } from '../src/services/core/registered.js';
|
|
17
17
|
import { GITLAB } from '../src/services/gitlab.js';
|
|
18
18
|
import { GITHUB } from '../src/services/github.js';
|
|
19
|
+
import { derivePermissionsOverrideSigningKey, verifyPermissionsOverrideJwt, } from '../src/gateway/permissionsOverride.js';
|
|
19
20
|
import { TELEGRAM } from '../src/services/telegram.js';
|
|
20
21
|
import { deleteRegisteredService, loadRegisteredServices, saveRegisteredService, } from '../src/configDataStore.js';
|
|
21
22
|
import { loadRegisteredServicesIntoServiceRegistry } from '../src/serviceRegistry.js';
|
|
22
23
|
// Use a fixed test key for deterministic test behavior (32 bytes = 256 bits, base64 encoded)
|
|
23
24
|
const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
|
|
24
|
-
|
|
25
|
-
const storage =
|
|
25
|
+
function writeSecureFile(path, content) {
|
|
26
|
+
const storage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
26
27
|
storage.writeFile(path, content);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
const storage =
|
|
29
|
+
function readSecureFile(path) {
|
|
30
|
+
const storage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
30
31
|
return storage.readFile(path);
|
|
31
32
|
}
|
|
32
33
|
function getCliPath() {
|
|
@@ -290,6 +291,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
290
291
|
get permissionsConfigPath() {
|
|
291
292
|
return overrides.permissionsConfigOverride ?? join(directory, 'permissions.json');
|
|
292
293
|
},
|
|
294
|
+
get extensionsDirectoryPath() {
|
|
295
|
+
return join(directory, 'extensions');
|
|
296
|
+
},
|
|
293
297
|
curlCommand: overrides.curlCommand ?? defaultConfig.curlCommand,
|
|
294
298
|
encryptionKeyOverride: overrides.encryptionKeyOverride ?? TEST_ENCRYPTION_KEY,
|
|
295
299
|
serviceName: overrides.serviceName ?? defaultConfig.serviceName,
|
|
@@ -301,6 +305,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
301
305
|
gatewayUrl: overrides.gatewayUrl ?? null,
|
|
302
306
|
gatewayListenHost: overrides.gatewayListenHost ?? 'localhost',
|
|
303
307
|
gatewayListenPort: overrides.gatewayListenPort ?? 1989,
|
|
308
|
+
gatewayPassword: overrides.gatewayPassword ?? null,
|
|
309
|
+
gatewayListenPassword: overrides.gatewayListenPassword ?? null,
|
|
310
|
+
gatewayPermissionsOverride: overrides.gatewayPermissionsOverride ?? null,
|
|
304
311
|
checkSensitiveFilePermissions: () => undefined,
|
|
305
312
|
checkSystemPrerequisites: () => undefined,
|
|
306
313
|
};
|
|
@@ -403,7 +410,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
403
410
|
});
|
|
404
411
|
it('should include services with stored credentials when using --viable', async () => {
|
|
405
412
|
const storePath = join(tempDir, 'credentials.json');
|
|
406
|
-
|
|
413
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
407
414
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
408
415
|
}));
|
|
409
416
|
const deps = createMockDependencies();
|
|
@@ -414,7 +421,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
414
421
|
});
|
|
415
422
|
it('should include services with browser auth when using --viable', async () => {
|
|
416
423
|
const storePath = join(tempDir, 'credentials.json');
|
|
417
|
-
|
|
424
|
+
writeSecureFile(storePath, '{}');
|
|
418
425
|
// The default mock slack service has getSession defined, so it supports browser auth
|
|
419
426
|
// Ensure a graphical environment is available so browser auth is considered viable
|
|
420
427
|
const originalDisplay = process.env.DISPLAY;
|
|
@@ -437,7 +444,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
437
444
|
});
|
|
438
445
|
it('should exclude services without credentials or browser auth when using --viable', async () => {
|
|
439
446
|
const storePath = join(tempDir, 'credentials.json');
|
|
440
|
-
|
|
447
|
+
writeSecureFile(storePath, '{}');
|
|
441
448
|
const noLoginService = {
|
|
442
449
|
name: 'nologin',
|
|
443
450
|
displayName: 'No Login Service',
|
|
@@ -463,7 +470,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
463
470
|
});
|
|
464
471
|
it('should exclude browser-capable services when browser is disabled and no credentials with --viable', async () => {
|
|
465
472
|
const storePath = join(tempDir, 'credentials.json');
|
|
466
|
-
|
|
473
|
+
writeSecureFile(storePath, '{}');
|
|
467
474
|
const deps = createMockDependencies({
|
|
468
475
|
config: createMockConfig({ browserDisabled: true }),
|
|
469
476
|
});
|
|
@@ -474,7 +481,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
474
481
|
});
|
|
475
482
|
it('should exclude browser-capable services when no graphical environment and no credentials with --viable', async () => {
|
|
476
483
|
const storePath = join(tempDir, 'credentials.json');
|
|
477
|
-
|
|
484
|
+
writeSecureFile(storePath, '{}');
|
|
478
485
|
const originalPlatform = process.platform;
|
|
479
486
|
const originalDisplay = process.env.DISPLAY;
|
|
480
487
|
const originalWayland = process.env.WAYLAND_DISPLAY;
|
|
@@ -506,7 +513,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
506
513
|
});
|
|
507
514
|
it('should include services with credentials even when no graphical environment with --viable', async () => {
|
|
508
515
|
const storePath = join(tempDir, 'credentials.json');
|
|
509
|
-
|
|
516
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
510
517
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
511
518
|
}));
|
|
512
519
|
const originalPlatform = process.platform;
|
|
@@ -540,7 +547,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
540
547
|
});
|
|
541
548
|
it('should include services with credentials even when browser is disabled with --viable', async () => {
|
|
542
549
|
const storePath = join(tempDir, 'credentials.json');
|
|
543
|
-
|
|
550
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
544
551
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
545
552
|
}));
|
|
546
553
|
const deps = createMockDependencies({
|
|
@@ -553,7 +560,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
553
560
|
});
|
|
554
561
|
it('should combine --builtin and --viable filters', async () => {
|
|
555
562
|
const storePath = join(tempDir, 'credentials.json');
|
|
556
|
-
|
|
563
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
557
564
|
'my-gitlab': {
|
|
558
565
|
objectType: 'rawCurl',
|
|
559
566
|
curlArguments: ['-H', 'PRIVATE-TOKEN: token'],
|
|
@@ -587,7 +594,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
587
594
|
describe('services info command', () => {
|
|
588
595
|
it('should show login options, credentials status, and developer notes', async () => {
|
|
589
596
|
const storePath = join(tempDir, 'credentials.json');
|
|
590
|
-
|
|
597
|
+
writeSecureFile(storePath, '{}');
|
|
591
598
|
const deps = createMockDependencies();
|
|
592
599
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
593
600
|
expect(logs).toHaveLength(1);
|
|
@@ -601,7 +608,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
601
608
|
});
|
|
602
609
|
it('should show auth set only for services without browser login', async () => {
|
|
603
610
|
const storePath = join(tempDir, 'credentials.json');
|
|
604
|
-
|
|
611
|
+
writeSecureFile(storePath, '{}');
|
|
605
612
|
const noLoginService = {
|
|
606
613
|
name: 'nologin',
|
|
607
614
|
displayName: 'No Login Service',
|
|
@@ -626,7 +633,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
626
633
|
});
|
|
627
634
|
it('should not list browser in authOptions when LATCHKEY_DISABLE_BROWSER is in effect', async () => {
|
|
628
635
|
const storePath = join(tempDir, 'credentials.json');
|
|
629
|
-
|
|
636
|
+
writeSecureFile(storePath, '{}');
|
|
630
637
|
const deps = createMockDependencies({
|
|
631
638
|
config: createMockConfig({ browserDisabled: true }),
|
|
632
639
|
});
|
|
@@ -636,7 +643,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
636
643
|
});
|
|
637
644
|
it('should show valid credentials status when credentials are valid', async () => {
|
|
638
645
|
const storePath = join(tempDir, 'credentials.json');
|
|
639
|
-
|
|
646
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
640
647
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
641
648
|
}));
|
|
642
649
|
const deps = createMockDependencies();
|
|
@@ -652,7 +659,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
652
659
|
});
|
|
653
660
|
it('should show type as registered for registered services', async () => {
|
|
654
661
|
const storePath = join(tempDir, 'credentials.json');
|
|
655
|
-
|
|
662
|
+
writeSecureFile(storePath, '{}');
|
|
656
663
|
const registeredService = new RegisteredService('my-gitlab', 'https://gitlab.example.com');
|
|
657
664
|
const deps = createMockDependencies();
|
|
658
665
|
deps.registry.addService(registeredService);
|
|
@@ -664,12 +671,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
664
671
|
describe('clear command', () => {
|
|
665
672
|
it('should delete credentials for a service', async () => {
|
|
666
673
|
const storePath = join(tempDir, 'credentials.json');
|
|
667
|
-
|
|
674
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
668
675
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
669
676
|
}));
|
|
670
677
|
const deps = createMockDependencies();
|
|
671
678
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
672
|
-
const storedData = JSON.parse(
|
|
679
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
673
680
|
expect(storedData.slack).toBeUndefined();
|
|
674
681
|
});
|
|
675
682
|
it('should return error for unknown service', async () => {
|
|
@@ -680,13 +687,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
680
687
|
});
|
|
681
688
|
it('should preserve other services when clearing one', async () => {
|
|
682
689
|
const storePath = join(tempDir, 'credentials.json');
|
|
683
|
-
|
|
690
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
684
691
|
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
685
692
|
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
686
693
|
}));
|
|
687
694
|
const deps = createMockDependencies();
|
|
688
695
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
689
|
-
const storedData = JSON.parse(
|
|
696
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
690
697
|
expect(storedData.slack).toBeUndefined();
|
|
691
698
|
expect(storedData.discord).toBeDefined();
|
|
692
699
|
expect(storedData.discord?.token).toBe('discord-token');
|
|
@@ -694,8 +701,8 @@ describe('CLI commands with dependency injection', () => {
|
|
|
694
701
|
it('should delete both store and browser state with -y flag', async () => {
|
|
695
702
|
const storePath = join(tempDir, 'credentials.json');
|
|
696
703
|
const browserStatePath = join(tempDir, 'browser_state.json');
|
|
697
|
-
|
|
698
|
-
|
|
704
|
+
writeSecureFile(storePath, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
705
|
+
writeSecureFile(browserStatePath, '{}');
|
|
699
706
|
const deps = createMockDependencies();
|
|
700
707
|
await runCommand(['auth', 'clear', '-y'], deps);
|
|
701
708
|
expect(existsSync(storePath)).toBe(false);
|
|
@@ -705,7 +712,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
705
712
|
describe('auth list command', () => {
|
|
706
713
|
it('should list stored credentials with their status', async () => {
|
|
707
714
|
const storePath = join(tempDir, 'credentials.json');
|
|
708
|
-
|
|
715
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
709
716
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
710
717
|
}));
|
|
711
718
|
const deps = createMockDependencies();
|
|
@@ -719,7 +726,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
719
726
|
});
|
|
720
727
|
it('should output empty object when no credentials are stored', async () => {
|
|
721
728
|
const storePath = join(tempDir, 'credentials.json');
|
|
722
|
-
|
|
729
|
+
writeSecureFile(storePath, '{}');
|
|
723
730
|
const deps = createMockDependencies();
|
|
724
731
|
await runCommand(['auth', 'list'], deps);
|
|
725
732
|
expect(logs).toHaveLength(1);
|
|
@@ -728,7 +735,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
728
735
|
});
|
|
729
736
|
it('should treat unknown services as valid', async () => {
|
|
730
737
|
const storePath = join(tempDir, 'credentials.json');
|
|
731
|
-
|
|
738
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
732
739
|
unknown: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: secret'] },
|
|
733
740
|
}));
|
|
734
741
|
const deps = createMockDependencies();
|
|
@@ -744,11 +751,11 @@ describe('CLI commands with dependency injection', () => {
|
|
|
744
751
|
describe('auth set command', () => {
|
|
745
752
|
it('should store raw curl credentials', async () => {
|
|
746
753
|
const storePath = join(tempDir, 'credentials.json');
|
|
747
|
-
|
|
754
|
+
writeSecureFile(storePath, '{}');
|
|
748
755
|
const deps = createMockDependencies();
|
|
749
756
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: secret', '-H', 'X-Other: value'], deps);
|
|
750
757
|
expect(logs).toContain('Credentials stored.');
|
|
751
|
-
const storedData = JSON.parse(
|
|
758
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
752
759
|
expect(storedData.slack).toEqual({
|
|
753
760
|
objectType: 'rawCurl',
|
|
754
761
|
curlArguments: ['-H', 'X-Token: secret', '-H', 'X-Other: value'],
|
|
@@ -771,13 +778,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
771
778
|
});
|
|
772
779
|
it('should overwrite existing credentials', async () => {
|
|
773
780
|
const storePath = join(tempDir, 'credentials.json');
|
|
774
|
-
|
|
781
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
775
782
|
slack: { objectType: 'slack', token: 'old-token', dCookie: 'old-cookie' },
|
|
776
783
|
}));
|
|
777
784
|
const deps = createMockDependencies();
|
|
778
785
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: new-secret'], deps);
|
|
779
786
|
expect(logs).toContain('Credentials stored.');
|
|
780
|
-
const storedData = JSON.parse(
|
|
787
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
781
788
|
expect(storedData.slack).toEqual({
|
|
782
789
|
objectType: 'rawCurl',
|
|
783
790
|
curlArguments: ['-H', 'X-Token: new-secret'],
|
|
@@ -787,13 +794,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
787
794
|
describe('auth set-nocurl command', () => {
|
|
788
795
|
it('should store telegram bot credentials', async () => {
|
|
789
796
|
const storePath = join(tempDir, 'credentials.json');
|
|
790
|
-
|
|
797
|
+
writeSecureFile(storePath, '{}');
|
|
791
798
|
const deps = createMockDependencies({
|
|
792
799
|
registry: new ServiceRegistry([TELEGRAM]),
|
|
793
800
|
});
|
|
794
801
|
await runCommand(['auth', 'set-nocurl', 'telegram', '123456:ABC-DEF'], deps);
|
|
795
802
|
expect(logs).toContain('Credentials stored.');
|
|
796
|
-
const storedData = JSON.parse(
|
|
803
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
797
804
|
expect(storedData.telegram).toEqual({
|
|
798
805
|
objectType: 'telegramBot',
|
|
799
806
|
token: '123456:ABC-DEF',
|
|
@@ -827,7 +834,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
827
834
|
describe('curl command', () => {
|
|
828
835
|
it('should pass arguments to subprocess', async () => {
|
|
829
836
|
const storePath = join(tempDir, 'credentials.json');
|
|
830
|
-
|
|
837
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
831
838
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
832
839
|
}));
|
|
833
840
|
const deps = createMockDependencies();
|
|
@@ -843,7 +850,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
843
850
|
});
|
|
844
851
|
it('should pass raw curl credentials to subprocess', async () => {
|
|
845
852
|
const storePath = join(tempDir, 'credentials.json');
|
|
846
|
-
|
|
853
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
847
854
|
slack: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Custom: header'] },
|
|
848
855
|
}));
|
|
849
856
|
const deps = createMockDependencies();
|
|
@@ -853,7 +860,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
853
860
|
});
|
|
854
861
|
it('should pass multiple arguments correctly', async () => {
|
|
855
862
|
const storePath = join(tempDir, 'credentials.json');
|
|
856
|
-
|
|
863
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
857
864
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
858
865
|
}));
|
|
859
866
|
const deps = createMockDependencies();
|
|
@@ -875,7 +882,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
875
882
|
});
|
|
876
883
|
it('should return subprocess exit code', async () => {
|
|
877
884
|
const storePath = join(tempDir, 'credentials.json');
|
|
878
|
-
|
|
885
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
879
886
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
880
887
|
}));
|
|
881
888
|
const deps = createMockDependencies({
|
|
@@ -905,7 +912,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
905
912
|
});
|
|
906
913
|
it('should pass through missing credentials when passthroughUnknown is enabled', async () => {
|
|
907
914
|
const storePath = join(tempDir, 'credentials.json');
|
|
908
|
-
|
|
915
|
+
writeSecureFile(storePath, '{}');
|
|
909
916
|
const deps = createMockDependencies({
|
|
910
917
|
config: createMockConfig({ passthroughUnknown: true }),
|
|
911
918
|
});
|
|
@@ -916,7 +923,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
916
923
|
});
|
|
917
924
|
it('should still inject credentials for known services when passthroughUnknown is enabled', async () => {
|
|
918
925
|
const storePath = join(tempDir, 'credentials.json');
|
|
919
|
-
|
|
926
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
920
927
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
921
928
|
}));
|
|
922
929
|
const deps = createMockDependencies({
|
|
@@ -928,7 +935,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
928
935
|
});
|
|
929
936
|
it('should read credentials from store and not call login', async () => {
|
|
930
937
|
const storePath = join(tempDir, 'credentials.json');
|
|
931
|
-
|
|
938
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
932
939
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
933
940
|
}));
|
|
934
941
|
const mockLogin = vi.fn();
|
|
@@ -957,14 +964,14 @@ describe('CLI commands with dependency injection', () => {
|
|
|
957
964
|
});
|
|
958
965
|
it('should return error when no credentials in store', async () => {
|
|
959
966
|
const storePath = join(tempDir, 'credentials.json');
|
|
960
|
-
|
|
967
|
+
writeSecureFile(storePath, '{}');
|
|
961
968
|
const deps = createMockDependencies();
|
|
962
969
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
963
970
|
expect(exitCode).toBe(1);
|
|
964
971
|
});
|
|
965
972
|
it('should inject telegram bot token into URL path', async () => {
|
|
966
973
|
const storePath = join(tempDir, 'credentials.json');
|
|
967
|
-
|
|
974
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
968
975
|
telegram: { objectType: 'telegramBot', token: '123456:ABC-DEF' },
|
|
969
976
|
}));
|
|
970
977
|
const deps = createMockDependencies({
|
|
@@ -976,7 +983,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
976
983
|
});
|
|
977
984
|
it('should work when service does not have getSession but credentials exist', async () => {
|
|
978
985
|
const storePath = join(tempDir, 'credentials.json');
|
|
979
|
-
|
|
986
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
980
987
|
nologin: { objectType: 'rawCurl', curlArguments: ['-H', 'X-API-Key: secret'] },
|
|
981
988
|
}));
|
|
982
989
|
const noLoginService = {
|
|
@@ -1005,7 +1012,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1005
1012
|
});
|
|
1006
1013
|
it('should reject request when permission check denies it', async () => {
|
|
1007
1014
|
const storePath = join(tempDir, 'credentials.json');
|
|
1008
|
-
|
|
1015
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1009
1016
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1010
1017
|
}));
|
|
1011
1018
|
const deps = createMockDependencies({
|
|
@@ -1018,7 +1025,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1018
1025
|
});
|
|
1019
1026
|
it('should allow request when permission check approves it', async () => {
|
|
1020
1027
|
const storePath = join(tempDir, 'credentials.json');
|
|
1021
|
-
|
|
1028
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1022
1029
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1023
1030
|
}));
|
|
1024
1031
|
const deps = createMockDependencies({
|
|
@@ -1030,7 +1037,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1030
1037
|
});
|
|
1031
1038
|
it('should exit with error when permission check fails', async () => {
|
|
1032
1039
|
const storePath = join(tempDir, 'credentials.json');
|
|
1033
|
-
|
|
1040
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1034
1041
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1035
1042
|
}));
|
|
1036
1043
|
const { PermissionCheckError } = await import('../src/permissions.js');
|
|
@@ -1070,7 +1077,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1070
1077
|
});
|
|
1071
1078
|
it('should return error when no graphical environment is available', async () => {
|
|
1072
1079
|
const storePath = join(tempDir, 'credentials.json');
|
|
1073
|
-
|
|
1080
|
+
writeSecureFile(storePath, '{}');
|
|
1074
1081
|
const originalPlatform = process.platform;
|
|
1075
1082
|
const originalDisplay = process.env.DISPLAY;
|
|
1076
1083
|
const originalWayland = process.env.WAYLAND_DISPLAY;
|
|
@@ -1235,7 +1242,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1235
1242
|
});
|
|
1236
1243
|
it('should not expose browser auth without --login-url', async () => {
|
|
1237
1244
|
const storePath = join(tempDir, 'credentials.json');
|
|
1238
|
-
|
|
1245
|
+
writeSecureFile(storePath, '{}');
|
|
1239
1246
|
const deps = createMockDependencies({
|
|
1240
1247
|
registry: new ServiceRegistry([GITLAB]),
|
|
1241
1248
|
});
|
|
@@ -1338,7 +1345,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1338
1345
|
], deps);
|
|
1339
1346
|
// Now store credentials for it
|
|
1340
1347
|
const storePath = join(tempDir, 'credentials.json');
|
|
1341
|
-
|
|
1348
|
+
writeSecureFile(storePath, '{}');
|
|
1342
1349
|
logs = [];
|
|
1343
1350
|
exitCode = null;
|
|
1344
1351
|
await runCommand(['auth', 'set', 'my-gitlab', '-H', 'PRIVATE-TOKEN: my-secret-token'], deps);
|
|
@@ -1370,7 +1377,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1370
1377
|
});
|
|
1371
1378
|
it('should not expose browser auth for service without family', async () => {
|
|
1372
1379
|
const storePath = join(tempDir, 'credentials.json');
|
|
1373
|
-
|
|
1380
|
+
writeSecureFile(storePath, '{}');
|
|
1374
1381
|
const deps = createMockDependencies({
|
|
1375
1382
|
registry: new ServiceRegistry([GITLAB]),
|
|
1376
1383
|
});
|
|
@@ -1389,7 +1396,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1389
1396
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1390
1397
|
// Store credentials
|
|
1391
1398
|
const storePath = join(tempDir, 'credentials.json');
|
|
1392
|
-
|
|
1399
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1393
1400
|
'my-api': {
|
|
1394
1401
|
objectType: 'rawCurl',
|
|
1395
1402
|
curlArguments: ['-H', 'Authorization: Bearer my-token'],
|
|
@@ -1443,7 +1450,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1443
1450
|
], deps);
|
|
1444
1451
|
// Store credentials
|
|
1445
1452
|
const storePath = join(tempDir, 'credentials.json');
|
|
1446
|
-
|
|
1453
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1447
1454
|
'my-gitlab': {
|
|
1448
1455
|
objectType: 'rawCurl',
|
|
1449
1456
|
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
@@ -1526,7 +1533,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1526
1533
|
], deps);
|
|
1527
1534
|
// Store credentials for it
|
|
1528
1535
|
const storePath = join(tempDir, 'credentials.json');
|
|
1529
|
-
|
|
1536
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1530
1537
|
'my-gitlab': {
|
|
1531
1538
|
objectType: 'rawCurl',
|
|
1532
1539
|
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
@@ -1559,7 +1566,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1559
1566
|
], deps);
|
|
1560
1567
|
// Store and then clear credentials
|
|
1561
1568
|
const storePath = join(tempDir, 'credentials.json');
|
|
1562
|
-
|
|
1569
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1563
1570
|
'my-gitlab': {
|
|
1564
1571
|
objectType: 'rawCurl',
|
|
1565
1572
|
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
@@ -1578,6 +1585,44 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1578
1585
|
expect(logs).toContain("Service 'my-gitlab' deregistered.");
|
|
1579
1586
|
});
|
|
1580
1587
|
});
|
|
1588
|
+
describe('gateway create-jwt command', () => {
|
|
1589
|
+
it('prints a JWT for an existing absolute path', async () => {
|
|
1590
|
+
const permissionsPath = join(tempDir, 'permissions.json');
|
|
1591
|
+
writeFileSync(permissionsPath, '{}');
|
|
1592
|
+
const deps = createMockDependencies();
|
|
1593
|
+
await runCommand(['gateway', 'create-jwt', permissionsPath], deps);
|
|
1594
|
+
expect(exitCode).toBeNull();
|
|
1595
|
+
expect(logs).toHaveLength(1);
|
|
1596
|
+
const jwt = logs[0];
|
|
1597
|
+
expect(jwt.split('.')).toHaveLength(3);
|
|
1598
|
+
const signingKey = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
1599
|
+
const payload = verifyPermissionsOverrideJwt(jwt, signingKey);
|
|
1600
|
+
expect(payload.permissionsConfig).toBe(permissionsPath);
|
|
1601
|
+
});
|
|
1602
|
+
it('refuses to issue a JWT when the path does not exist', async () => {
|
|
1603
|
+
const missingPath = join(tempDir, 'missing.json');
|
|
1604
|
+
const deps = createMockDependencies();
|
|
1605
|
+
await runCommand(['gateway', 'create-jwt', missingPath], deps);
|
|
1606
|
+
expect(exitCode).toBe(1);
|
|
1607
|
+
expect(errorLogs.some((message) => message.includes('does not exist'))).toBe(true);
|
|
1608
|
+
});
|
|
1609
|
+
it('issues a JWT without checking existence when --no-validate is given', async () => {
|
|
1610
|
+
const missingPath = join(tempDir, 'missing.json');
|
|
1611
|
+
const deps = createMockDependencies();
|
|
1612
|
+
await runCommand(['gateway', 'create-jwt', missingPath, '--no-validate'], deps);
|
|
1613
|
+
expect(exitCode).toBeNull();
|
|
1614
|
+
expect(logs).toHaveLength(1);
|
|
1615
|
+
const signingKey = derivePermissionsOverrideSigningKey(TEST_ENCRYPTION_KEY);
|
|
1616
|
+
const payload = verifyPermissionsOverrideJwt(logs[0], signingKey);
|
|
1617
|
+
expect(payload.permissionsConfig).toBe(missingPath);
|
|
1618
|
+
});
|
|
1619
|
+
it('rejects a non-absolute path', async () => {
|
|
1620
|
+
const deps = createMockDependencies();
|
|
1621
|
+
await runCommand(['gateway', 'create-jwt', 'relative.json', '--no-validate'], deps);
|
|
1622
|
+
expect(exitCode).toBe(1);
|
|
1623
|
+
expect(errorLogs.some((message) => message.includes('absolute'))).toBe(true);
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1581
1626
|
describe('gateway mode (LATCHKEY_GATEWAY)', () => {
|
|
1582
1627
|
const GATEWAY_URL = 'http://localhost:9000';
|
|
1583
1628
|
const originalFetch = globalThis.fetch;
|