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.
Files changed (119) hide show
  1. package/README.md +55 -5
  2. package/dist/scripts/cryptFile.js +2 -2
  3. package/dist/scripts/cryptFile.js.map +1 -1
  4. package/dist/scripts/recordBrowserSession.js +3 -2
  5. package/dist/scripts/recordBrowserSession.js.map +1 -1
  6. package/dist/src/cli.js +5 -4
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/cliCommands.d.ts +1 -1
  9. package/dist/src/cliCommands.d.ts.map +1 -1
  10. package/dist/src/cliCommands.js +44 -6
  11. package/dist/src/cliCommands.js.map +1 -1
  12. package/dist/src/config.d.ts +34 -0
  13. package/dist/src/config.d.ts.map +1 -1
  14. package/dist/src/config.js +53 -0
  15. package/dist/src/config.js.map +1 -1
  16. package/dist/src/curlInjection.d.ts +1 -1
  17. package/dist/src/curlInjection.d.ts.map +1 -1
  18. package/dist/src/curlInjection.js +16 -1
  19. package/dist/src/curlInjection.js.map +1 -1
  20. package/dist/src/encryptedStorage.d.ts +9 -25
  21. package/dist/src/encryptedStorage.d.ts.map +1 -1
  22. package/dist/src/encryptedStorage.js +9 -52
  23. package/dist/src/encryptedStorage.js.map +1 -1
  24. package/dist/src/encryption.d.ts +45 -0
  25. package/dist/src/encryption.d.ts.map +1 -1
  26. package/dist/src/encryption.js +69 -0
  27. package/dist/src/encryption.js.map +1 -1
  28. package/dist/src/gateway/client.d.ts +12 -2
  29. package/dist/src/gateway/client.d.ts.map +1 -1
  30. package/dist/src/gateway/client.js +31 -4
  31. package/dist/src/gateway/client.js.map +1 -1
  32. package/dist/src/gateway/extensions.d.ts +59 -0
  33. package/dist/src/gateway/extensions.d.ts.map +1 -0
  34. package/dist/src/gateway/extensions.js +170 -0
  35. package/dist/src/gateway/extensions.js.map +1 -0
  36. package/dist/src/gateway/gatewayEndpoint.d.ts +22 -1
  37. package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
  38. package/dist/src/gateway/gatewayEndpoint.js +52 -15
  39. package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
  40. package/dist/src/gateway/password.d.ts +16 -0
  41. package/dist/src/gateway/password.d.ts.map +1 -0
  42. package/dist/src/gateway/password.js +24 -0
  43. package/dist/src/gateway/password.js.map +1 -0
  44. package/dist/src/gateway/permissionsOverride.d.ts +65 -0
  45. package/dist/src/gateway/permissionsOverride.d.ts.map +1 -0
  46. package/dist/src/gateway/permissionsOverride.js +171 -0
  47. package/dist/src/gateway/permissionsOverride.js.map +1 -0
  48. package/dist/src/gateway/server.d.ts.map +1 -1
  49. package/dist/src/gateway/server.js +100 -15
  50. package/dist/src/gateway/server.js.map +1 -1
  51. package/dist/src/index.d.ts +2 -2
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +2 -2
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/oauthUtils.d.ts +11 -2
  56. package/dist/src/oauthUtils.d.ts.map +1 -1
  57. package/dist/src/oauthUtils.js +25 -4
  58. package/dist/src/oauthUtils.js.map +1 -1
  59. package/dist/src/permissions.d.ts +3 -6
  60. package/dist/src/permissions.d.ts.map +1 -1
  61. package/dist/src/permissions.js +6 -13
  62. package/dist/src/permissions.js.map +1 -1
  63. package/dist/src/serviceRegistry.d.ts.map +1 -1
  64. package/dist/src/serviceRegistry.js +2 -1
  65. package/dist/src/serviceRegistry.js.map +1 -1
  66. package/dist/src/services/index.d.ts +1 -0
  67. package/dist/src/services/index.d.ts.map +1 -1
  68. package/dist/src/services/index.js +1 -0
  69. package/dist/src/services/index.js.map +1 -1
  70. package/dist/src/services/notion-mcp.d.ts +29 -0
  71. package/dist/src/services/notion-mcp.d.ts.map +1 -0
  72. package/dist/src/services/notion-mcp.js +156 -0
  73. package/dist/src/services/notion-mcp.js.map +1 -0
  74. package/dist/src/services/notion.d.ts.map +1 -1
  75. package/dist/src/services/notion.js +3 -2
  76. package/dist/src/services/notion.js.map +1 -1
  77. package/dist/src/version.d.ts +1 -1
  78. package/dist/src/version.js +1 -1
  79. package/dist/tests/apiCredentialStore.test.js +2 -2
  80. package/dist/tests/apiCredentialStore.test.js.map +1 -1
  81. package/dist/tests/cli.test.js +98 -53
  82. package/dist/tests/cli.test.js.map +1 -1
  83. package/dist/tests/config.test.js +37 -0
  84. package/dist/tests/config.test.js.map +1 -1
  85. package/dist/tests/encryptedStorage.test.js +19 -39
  86. package/dist/tests/encryptedStorage.test.js.map +1 -1
  87. package/dist/tests/gateway.test.js +184 -7
  88. package/dist/tests/gateway.test.js.map +1 -1
  89. package/dist/tests/gatewayClient.test.js +74 -0
  90. package/dist/tests/gatewayClient.test.js.map +1 -1
  91. package/dist/tests/gatewayExtensions.test.d.ts +2 -0
  92. package/dist/tests/gatewayExtensions.test.d.ts.map +1 -0
  93. package/dist/tests/gatewayExtensions.test.js +398 -0
  94. package/dist/tests/gatewayExtensions.test.js.map +1 -0
  95. package/dist/tests/latchkeyEndpoint.test.js +7 -6
  96. package/dist/tests/latchkeyEndpoint.test.js.map +1 -1
  97. package/dist/tests/migrations.test.js +2 -2
  98. package/dist/tests/migrations.test.js.map +1 -1
  99. package/dist/tests/oauthUtils.test.d.ts +2 -0
  100. package/dist/tests/oauthUtils.test.d.ts.map +1 -0
  101. package/dist/tests/oauthUtils.test.js +63 -0
  102. package/dist/tests/oauthUtils.test.js.map +1 -0
  103. package/dist/tests/permissions.test.js +14 -10
  104. package/dist/tests/permissions.test.js.map +1 -1
  105. package/dist/tests/permissionsOverride.test.d.ts +2 -0
  106. package/dist/tests/permissionsOverride.test.d.ts.map +1 -0
  107. package/dist/tests/permissionsOverride.test.js +136 -0
  108. package/dist/tests/permissionsOverride.test.js.map +1 -0
  109. package/dist/tests/resolveEncryptionKey.test.d.ts +2 -0
  110. package/dist/tests/resolveEncryptionKey.test.d.ts.map +1 -0
  111. package/dist/tests/resolveEncryptionKey.test.js +26 -0
  112. package/dist/tests/resolveEncryptionKey.test.js.map +1 -0
  113. package/dist/tests/sharedOperations.test.js +34 -50
  114. package/dist/tests/sharedOperations.test.js.map +1 -1
  115. package/package.json +2 -2
  116. package/dist/tests/encryptedStorageKeyGeneration.test.d.ts +0 -2
  117. package/dist/tests/encryptedStorageKeyGeneration.test.d.ts.map +0 -1
  118. package/dist/tests/encryptedStorageKeyGeneration.test.js +0 -22
  119. package/dist/tests/encryptedStorageKeyGeneration.test.js.map +0 -1
@@ -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
- async function writeSecureFile(path, content) {
25
- const storage = await EncryptedStorage.create({ encryptionKeyOverride: TEST_ENCRYPTION_KEY });
25
+ function writeSecureFile(path, content) {
26
+ const storage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
26
27
  storage.writeFile(path, content);
27
28
  }
28
- async function readSecureFile(path) {
29
- const storage = await EncryptedStorage.create({ encryptionKeyOverride: TEST_ENCRYPTION_KEY });
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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((await readSecureFile(storePath)) ?? '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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((await readSecureFile(storePath)) ?? '{}');
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
- await writeSecureFile(storePath, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
698
- await writeSecureFile(browserStatePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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((await readSecureFile(storePath)) ?? '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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((await readSecureFile(storePath)) ?? '{}');
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
- await writeSecureFile(storePath, '{}');
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((await readSecureFile(storePath)) ?? '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, '{}');
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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
- await writeSecureFile(storePath, JSON.stringify({
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;