latchkey 2.6.1 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +50 -1
  2. package/dist/scripts/recordBrowserSession.js +3 -3
  3. package/dist/scripts/recordBrowserSession.js.map +1 -1
  4. package/dist/src/{apiCredentials.d.ts → apiCredentials/base.d.ts} +6 -6
  5. package/dist/src/apiCredentials/base.d.ts.map +1 -0
  6. package/dist/src/{apiCredentials.js → apiCredentials/base.js} +5 -5
  7. package/dist/src/apiCredentials/base.js.map +1 -0
  8. package/dist/src/{apiCredentialsSerialization.d.ts → apiCredentials/serialization.d.ts} +5 -5
  9. package/dist/src/apiCredentials/serialization.d.ts.map +1 -0
  10. package/dist/src/{apiCredentialsSerialization.js → apiCredentials/serialization.js} +9 -9
  11. package/dist/src/apiCredentials/serialization.js.map +1 -0
  12. package/dist/src/{apiCredentialStore.d.ts → apiCredentials/store.d.ts} +3 -3
  13. package/dist/src/apiCredentials/store.d.ts.map +1 -0
  14. package/dist/src/{apiCredentialStore.js → apiCredentials/store.js} +2 -2
  15. package/dist/src/apiCredentials/store.js.map +1 -0
  16. package/dist/src/apiCredentials/utils.d.ts +13 -0
  17. package/dist/src/apiCredentials/utils.d.ts.map +1 -0
  18. package/dist/src/apiCredentials/utils.js +27 -0
  19. package/dist/src/apiCredentials/utils.js.map +1 -0
  20. package/dist/src/browserConfig.d.ts +5 -1
  21. package/dist/src/browserConfig.d.ts.map +1 -1
  22. package/dist/src/browserConfig.js +17 -3
  23. package/dist/src/browserConfig.js.map +1 -1
  24. package/dist/src/cli.js +42 -39
  25. package/dist/src/cli.js.map +1 -1
  26. package/dist/src/cliCommands.d.ts +5 -2
  27. package/dist/src/cliCommands.d.ts.map +1 -1
  28. package/dist/src/cliCommands.js +270 -190
  29. package/dist/src/cliCommands.js.map +1 -1
  30. package/dist/src/config.d.ts +32 -2
  31. package/dist/src/config.d.ts.map +1 -1
  32. package/dist/src/config.js +107 -20
  33. package/dist/src/config.js.map +1 -1
  34. package/dist/src/configDataStore.d.ts +44 -0
  35. package/dist/src/configDataStore.d.ts.map +1 -1
  36. package/dist/src/configDataStore.js +27 -0
  37. package/dist/src/configDataStore.js.map +1 -1
  38. package/dist/src/curl.d.ts +41 -8
  39. package/dist/src/curl.d.ts.map +1 -1
  40. package/dist/src/curl.js +80 -75
  41. package/dist/src/curl.js.map +1 -1
  42. package/dist/src/curlInjection.d.ts +46 -0
  43. package/dist/src/curlInjection.d.ts.map +1 -0
  44. package/dist/src/curlInjection.js +99 -0
  45. package/dist/src/curlInjection.js.map +1 -0
  46. package/dist/src/errorMessages.d.ts +14 -0
  47. package/dist/src/errorMessages.d.ts.map +1 -0
  48. package/dist/src/errorMessages.js +22 -0
  49. package/dist/src/errorMessages.js.map +1 -0
  50. package/dist/src/gateway/client.d.ts +32 -0
  51. package/dist/src/gateway/client.d.ts.map +1 -0
  52. package/dist/src/gateway/client.js +89 -0
  53. package/dist/src/gateway/client.js.map +1 -0
  54. package/dist/src/gateway/gatewayEndpoint.d.ts +43 -0
  55. package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -0
  56. package/dist/src/gateway/gatewayEndpoint.js +297 -0
  57. package/dist/src/gateway/gatewayEndpoint.js.map +1 -0
  58. package/dist/src/gateway/latchkeyEndpoint.d.ts +105 -0
  59. package/dist/src/gateway/latchkeyEndpoint.d.ts.map +1 -0
  60. package/dist/src/gateway/latchkeyEndpoint.js +144 -0
  61. package/dist/src/gateway/latchkeyEndpoint.js.map +1 -0
  62. package/dist/src/gateway/server.d.ts +20 -0
  63. package/dist/src/gateway/server.d.ts.map +1 -0
  64. package/dist/src/gateway/server.js +90 -0
  65. package/dist/src/gateway/server.js.map +1 -0
  66. package/dist/src/index.d.ts +4 -4
  67. package/dist/src/index.d.ts.map +1 -1
  68. package/dist/src/index.js +5 -5
  69. package/dist/src/index.js.map +1 -1
  70. package/dist/src/permissions.d.ts.map +1 -1
  71. package/dist/src/permissions.js +4 -2
  72. package/dist/src/permissions.js.map +1 -1
  73. package/dist/src/playwrightDownload.d.ts +1 -1
  74. package/dist/src/playwrightDownload.d.ts.map +1 -1
  75. package/dist/src/playwrightDownload.js +5 -4
  76. package/dist/src/playwrightDownload.js.map +1 -1
  77. package/dist/src/playwrightLoader.d.ts +17 -0
  78. package/dist/src/playwrightLoader.d.ts.map +1 -0
  79. package/dist/src/playwrightLoader.js +47 -0
  80. package/dist/src/playwrightLoader.js.map +1 -0
  81. package/dist/src/playwrightUtils.d.ts.map +1 -1
  82. package/dist/src/playwrightUtils.js +2 -1
  83. package/dist/src/playwrightUtils.js.map +1 -1
  84. package/dist/src/{registry.d.ts → serviceRegistry.d.ts} +4 -4
  85. package/dist/src/serviceRegistry.d.ts.map +1 -0
  86. package/dist/src/{registry.js → serviceRegistry.js} +4 -4
  87. package/dist/src/serviceRegistry.js.map +1 -0
  88. package/dist/src/services/aws.d.ts +2 -2
  89. package/dist/src/services/aws.d.ts.map +1 -1
  90. package/dist/src/services/aws.js +17 -10
  91. package/dist/src/services/aws.js.map +1 -1
  92. package/dist/src/services/core/base.d.ts +2 -2
  93. package/dist/src/services/core/base.d.ts.map +1 -1
  94. package/dist/src/services/core/base.js +3 -3
  95. package/dist/src/services/core/base.js.map +1 -1
  96. package/dist/src/services/core/registered.d.ts +2 -2
  97. package/dist/src/services/core/registered.d.ts.map +1 -1
  98. package/dist/src/services/core/registered.js +2 -2
  99. package/dist/src/services/core/registered.js.map +1 -1
  100. package/dist/src/services/discord.d.ts +1 -1
  101. package/dist/src/services/discord.d.ts.map +1 -1
  102. package/dist/src/services/discord.js +1 -1
  103. package/dist/src/services/discord.js.map +1 -1
  104. package/dist/src/services/dropbox.d.ts +1 -1
  105. package/dist/src/services/dropbox.d.ts.map +1 -1
  106. package/dist/src/services/dropbox.js +1 -1
  107. package/dist/src/services/dropbox.js.map +1 -1
  108. package/dist/src/services/github.d.ts +2 -2
  109. package/dist/src/services/github.d.ts.map +1 -1
  110. package/dist/src/services/github.js +2 -2
  111. package/dist/src/services/github.js.map +1 -1
  112. package/dist/src/services/google/base.d.ts +2 -2
  113. package/dist/src/services/google/base.d.ts.map +1 -1
  114. package/dist/src/services/google/base.js +3 -3
  115. package/dist/src/services/google/base.js.map +1 -1
  116. package/dist/src/services/google/directions.d.ts +1 -1
  117. package/dist/src/services/google/directions.d.ts.map +1 -1
  118. package/dist/src/services/linear.d.ts +1 -1
  119. package/dist/src/services/linear.d.ts.map +1 -1
  120. package/dist/src/services/linear.js +1 -1
  121. package/dist/src/services/linear.js.map +1 -1
  122. package/dist/src/services/notion.d.ts +1 -1
  123. package/dist/src/services/notion.d.ts.map +1 -1
  124. package/dist/src/services/notion.js +1 -1
  125. package/dist/src/services/notion.js.map +1 -1
  126. package/dist/src/services/sentry.d.ts +2 -2
  127. package/dist/src/services/sentry.d.ts.map +1 -1
  128. package/dist/src/services/sentry.js +6 -3
  129. package/dist/src/services/sentry.js.map +1 -1
  130. package/dist/src/services/slack.d.ts +3 -3
  131. package/dist/src/services/slack.d.ts.map +1 -1
  132. package/dist/src/services/slack.js +5 -5
  133. package/dist/src/services/slack.js.map +1 -1
  134. package/dist/src/services/telegram.d.ts +2 -2
  135. package/dist/src/services/telegram.d.ts.map +1 -1
  136. package/dist/src/services/telegram.js +2 -2
  137. package/dist/src/services/telegram.js.map +1 -1
  138. package/dist/src/sharedOperations.d.ts +44 -0
  139. package/dist/src/sharedOperations.d.ts.map +1 -0
  140. package/dist/src/sharedOperations.js +131 -0
  141. package/dist/src/sharedOperations.js.map +1 -0
  142. package/dist/src/version.d.ts +2 -0
  143. package/dist/src/version.d.ts.map +1 -0
  144. package/dist/src/version.js +4 -0
  145. package/dist/src/version.js.map +1 -0
  146. package/dist/tests/apiCredentialStore.test.js +2 -2
  147. package/dist/tests/apiCredentialStore.test.js.map +1 -1
  148. package/dist/tests/apiCredentials.test.js +37 -36
  149. package/dist/tests/apiCredentials.test.js.map +1 -1
  150. package/dist/tests/cli.test.js +240 -55
  151. package/dist/tests/cli.test.js.map +1 -1
  152. package/dist/tests/config.test.d.ts +2 -0
  153. package/dist/tests/config.test.d.ts.map +1 -0
  154. package/dist/tests/config.test.js +150 -0
  155. package/dist/tests/config.test.js.map +1 -0
  156. package/dist/tests/gateway.test.d.ts +2 -0
  157. package/dist/tests/gateway.test.d.ts.map +1 -0
  158. package/dist/tests/gateway.test.js +566 -0
  159. package/dist/tests/gateway.test.js.map +1 -0
  160. package/dist/tests/gatewayClient.test.d.ts +2 -0
  161. package/dist/tests/gatewayClient.test.d.ts.map +1 -0
  162. package/dist/tests/gatewayClient.test.js +85 -0
  163. package/dist/tests/gatewayClient.test.js.map +1 -0
  164. package/dist/tests/latchkeyEndpoint.test.d.ts +2 -0
  165. package/dist/tests/latchkeyEndpoint.test.d.ts.map +1 -0
  166. package/dist/tests/latchkeyEndpoint.test.js +385 -0
  167. package/dist/tests/latchkeyEndpoint.test.js.map +1 -0
  168. package/dist/tests/permissions.test.js +15 -0
  169. package/dist/tests/permissions.test.js.map +1 -1
  170. package/dist/tests/playwrightDownload.test.js +2 -2
  171. package/dist/tests/playwrightDownload.test.js.map +1 -1
  172. package/dist/tests/serviceRegistry.test.d.ts +2 -0
  173. package/dist/tests/serviceRegistry.test.d.ts.map +1 -0
  174. package/dist/tests/{registry.test.js → serviceRegistry.test.js} +17 -17
  175. package/dist/tests/serviceRegistry.test.js.map +1 -0
  176. package/dist/tests/servicesAgainstRecordings.test.js +3 -3
  177. package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
  178. package/dist/tests/sharedOperations.test.d.ts +2 -0
  179. package/dist/tests/sharedOperations.test.d.ts.map +1 -0
  180. package/dist/tests/sharedOperations.test.js +264 -0
  181. package/dist/tests/sharedOperations.test.js.map +1 -0
  182. package/package.json +10 -3
  183. package/dist/src/apiCredentialStore.d.ts.map +0 -1
  184. package/dist/src/apiCredentialStore.js.map +0 -1
  185. package/dist/src/apiCredentials.d.ts.map +0 -1
  186. package/dist/src/apiCredentials.js.map +0 -1
  187. package/dist/src/apiCredentialsSerialization.d.ts.map +0 -1
  188. package/dist/src/apiCredentialsSerialization.js.map +0 -1
  189. package/dist/src/registry.d.ts.map +0 -1
  190. package/dist/src/registry.js.map +0 -1
  191. package/dist/tests/registry.test.d.ts +0 -2
  192. package/dist/tests/registry.test.d.ts.map +0 -1
  193. package/dist/tests/registry.test.js.map +0 -1
@@ -5,12 +5,12 @@ import { tmpdir } from 'node:os';
5
5
  import { execSync } from 'node:child_process';
6
6
  import { Command } from 'commander';
7
7
  import { registerCommands } from '../src/cliCommands.js';
8
- import { extractUrlFromCurlArguments } from '../src/curl.js';
8
+ import { CurlParseError, extractUrlFromCurlArguments } from '../src/curl.js';
9
9
  import { hasGraphicalEnvironment } from '../src/playwrightUtils.js';
10
10
  import { EncryptedStorage } from '../src/encryptedStorage.js';
11
11
  import { Config } from '../src/config.js';
12
- import { Registry } from '../src/registry.js';
13
- import { ApiCredentialStatus } from '../src/apiCredentials.js';
12
+ import { ServiceRegistry } from '../src/serviceRegistry.js';
13
+ import { ApiCredentialStatus } from '../src/apiCredentials/base.js';
14
14
  import { SlackApiCredentials } from '../src/services/slack.js';
15
15
  import { NoCurlCredentialsNotSupportedError, Service } from '../src/services/core/base.js';
16
16
  import { RegisteredService } from '../src/services/core/registered.js';
@@ -18,7 +18,7 @@ import { GITLAB } from '../src/services/gitlab.js';
18
18
  import { GITHUB } from '../src/services/github.js';
19
19
  import { TELEGRAM } from '../src/services/telegram.js';
20
20
  import { deleteRegisteredService, loadRegisteredServices, saveRegisteredService, } from '../src/configDataStore.js';
21
- import { loadRegisteredServicesIntoRegistry } from '../src/registry.js';
21
+ import { loadRegisteredServicesIntoServiceRegistry } from '../src/serviceRegistry.js';
22
22
  // Use a fixed test key for deterministic test behavior (32 bytes = 256 bits, base64 encoded)
23
23
  const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
24
24
  async function writeSecureFile(path, content) {
@@ -98,13 +98,13 @@ describe('extractUrlFromCurlArguments', () => {
98
98
  const arguments_ = ['--header', 'Authorization: Bearer token', 'https://api.example.com'];
99
99
  expect(extractUrlFromCurlArguments(arguments_)).toBe('https://api.example.com');
100
100
  });
101
- it('should return null when no URL is present', () => {
101
+ it('should throw CurlParseError when no URL is present', () => {
102
102
  const arguments_ = ['-X', 'POST', '-H', 'Content-Type: application/json'];
103
- expect(extractUrlFromCurlArguments(arguments_)).toBeNull();
103
+ expect(() => extractUrlFromCurlArguments(arguments_)).toThrow(CurlParseError);
104
104
  });
105
- it('should return null for empty arguments', () => {
105
+ it('should throw CurlParseError for empty arguments', () => {
106
106
  const arguments_ = [];
107
- expect(extractUrlFromCurlArguments(arguments_)).toBeNull();
107
+ expect(() => extractUrlFromCurlArguments(arguments_)).toThrow(CurlParseError);
108
108
  });
109
109
  it('should handle verbose flag', () => {
110
110
  let arguments_ = ['-v', 'https://api.example.com'];
@@ -118,6 +118,16 @@ describe('extractUrlFromCurlArguments', () => {
118
118
  const arguments_ = ['-k', '--compressed', '-s', '-i', 'https://api.example.com'];
119
119
  expect(extractUrlFromCurlArguments(arguments_)).toBe('https://api.example.com');
120
120
  });
121
+ it('should return the raw arg for schemeless URLs (curl defaults to http://)', () => {
122
+ expect(extractUrlFromCurlArguments(['www.seznam.cz'])).toBe('www.seznam.cz');
123
+ expect(extractUrlFromCurlArguments(['-X', 'POST', 'api.example.com/path'])).toBe('api.example.com/path');
124
+ });
125
+ it('should return null for non-http(s) schemes', () => {
126
+ expect(extractUrlFromCurlArguments(['ftp://example.com/'])).toBeNull();
127
+ });
128
+ it('should propagate CurlParseError for malformed arguments', () => {
129
+ expect(() => extractUrlFromCurlArguments(['-H', 'no-colon-here'])).toThrow(CurlParseError);
130
+ });
121
131
  });
122
132
  describe('hasGraphicalEnvironment', () => {
123
133
  const originalPlatform = process.platform;
@@ -258,6 +268,10 @@ describe('CLI commands with dependency injection', () => {
258
268
  browserDisabled: overrides.browserDisabled ?? false,
259
269
  countingDisabled: overrides.countingDisabled ?? false,
260
270
  permissionsDoNotUseBuiltinSchemas: overrides.permissionsDoNotUseBuiltinSchemas ?? false,
271
+ passthroughUnknown: overrides.passthroughUnknown ?? false,
272
+ gatewayUrl: overrides.gatewayUrl ?? null,
273
+ gatewayListenHost: overrides.gatewayListenHost ?? 'localhost',
274
+ gatewayListenPort: overrides.gatewayListenPort ?? 1989,
261
275
  checkSensitiveFilePermissions: () => undefined,
262
276
  checkSystemPrerequisites: () => undefined,
263
277
  };
@@ -270,7 +284,7 @@ describe('CLI commands with dependency injection', () => {
270
284
  loginUrl: 'https://slack.com/signin',
271
285
  info: 'Test info for Slack service.',
272
286
  credentialCheckCurlArguments: ['https://slack.com/api/auth.test'],
273
- checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
287
+ checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Valid),
274
288
  setCredentialsExample(serviceName) {
275
289
  return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
276
290
  },
@@ -281,7 +295,7 @@ describe('CLI commands with dependency injection', () => {
281
295
  login: vi.fn().mockResolvedValue(new SlackApiCredentials('xoxc-test-token', 'test-cookie')),
282
296
  }),
283
297
  };
284
- const mockRegistry = new Registry([mockSlackService]);
298
+ const mockRegistry = new ServiceRegistry([mockSlackService]);
285
299
  return {
286
300
  registry: mockRegistry,
287
301
  config: createMockConfig(),
@@ -289,6 +303,7 @@ describe('CLI commands with dependency injection', () => {
289
303
  capturedArgs.push(...args);
290
304
  return { returncode: 0, stdout: '', stderr: '' };
291
305
  },
306
+ runCurlAsync: () => Promise.resolve({ returncode: 0, stdout: Buffer.from(''), stderr: '' }),
292
307
  checkPermission: () => Promise.resolve(true),
293
308
  confirm: () => Promise.resolve(true),
294
309
  exit: (code) => {
@@ -301,6 +316,7 @@ describe('CLI commands with dependency injection', () => {
301
316
  errorLog: (message) => {
302
317
  errorLogs.push(message);
303
318
  },
319
+ version: '0.0.0-test',
304
320
  ...overrides,
305
321
  };
306
322
  }
@@ -400,7 +416,7 @@ describe('CLI commands with dependency injection', () => {
400
416
  loginUrl: 'https://nologin.example.com',
401
417
  info: 'A service without browser login support.',
402
418
  credentialCheckCurlArguments: [],
403
- checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Missing),
419
+ checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Missing),
404
420
  setCredentialsExample(serviceName) {
405
421
  return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
406
422
  },
@@ -409,7 +425,7 @@ describe('CLI commands with dependency injection', () => {
409
425
  },
410
426
  };
411
427
  const deps = createMockDependencies({
412
- registry: new Registry([noLoginService]),
428
+ registry: new ServiceRegistry([noLoginService]),
413
429
  });
414
430
  await runCommand(['services', 'list', '--viable'], deps);
415
431
  expect(logs).toHaveLength(1);
@@ -564,7 +580,7 @@ describe('CLI commands with dependency injection', () => {
564
580
  loginUrl: 'https://nologin.example.com',
565
581
  info: 'A service without browser login support.',
566
582
  credentialCheckCurlArguments: [],
567
- checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Missing),
583
+ checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Missing),
568
584
  setCredentialsExample(serviceName) {
569
585
  return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
570
586
  },
@@ -573,7 +589,7 @@ describe('CLI commands with dependency injection', () => {
573
589
  },
574
590
  };
575
591
  const deps = createMockDependencies({
576
- registry: new Registry([noLoginService]),
592
+ registry: new ServiceRegistry([noLoginService]),
577
593
  });
578
594
  await runCommand(['services', 'info', 'nologin'], deps);
579
595
  const info = JSON.parse(logs[0] ?? '');
@@ -744,7 +760,7 @@ describe('CLI commands with dependency injection', () => {
744
760
  const storePath = join(tempDir, 'credentials.json');
745
761
  await writeSecureFile(storePath, '{}');
746
762
  const deps = createMockDependencies({
747
- registry: new Registry([TELEGRAM]),
763
+ registry: new ServiceRegistry([TELEGRAM]),
748
764
  });
749
765
  await runCommand(['auth', 'set-nocurl', 'telegram', '123456:ABC-DEF'], deps);
750
766
  expect(logs).toContain('Credentials stored.');
@@ -766,14 +782,14 @@ describe('CLI commands with dependency injection', () => {
766
782
  });
767
783
  it('should return error when telegram token is missing', async () => {
768
784
  const deps = createMockDependencies({
769
- registry: new Registry([TELEGRAM]),
785
+ registry: new ServiceRegistry([TELEGRAM]),
770
786
  });
771
787
  await runCommand(['auth', 'set-nocurl', 'telegram'], deps);
772
788
  expect(exitCode).toBe(1);
773
789
  });
774
790
  it('should return error when telegram token format is invalid', async () => {
775
791
  const deps = createMockDependencies({
776
- registry: new Registry([TELEGRAM]),
792
+ registry: new ServiceRegistry([TELEGRAM]),
777
793
  });
778
794
  await runCommand(['auth', 'set-nocurl', 'telegram', 'not-a-valid-token'], deps);
779
795
  expect(exitCode).toBe(1);
@@ -849,6 +865,38 @@ describe('CLI commands with dependency injection', () => {
849
865
  await runCommand(['curl', 'https://unknown-api.example.com'], deps);
850
866
  expect(exitCode).toBe(1);
851
867
  });
868
+ it('should pass through unknown service when passthroughUnknown is enabled', async () => {
869
+ const deps = createMockDependencies({
870
+ config: createMockConfig({ passthroughUnknown: true }),
871
+ });
872
+ await runCommand(['curl', 'https://unknown-api.example.com/test'], deps);
873
+ expect(exitCode).toBe(0);
874
+ expect(capturedArgs).toEqual(['https://unknown-api.example.com/test']);
875
+ expect(errorLogs).toHaveLength(0);
876
+ });
877
+ it('should pass through missing credentials when passthroughUnknown is enabled', async () => {
878
+ const storePath = join(tempDir, 'credentials.json');
879
+ await writeSecureFile(storePath, '{}');
880
+ const deps = createMockDependencies({
881
+ config: createMockConfig({ passthroughUnknown: true }),
882
+ });
883
+ await runCommand(['curl', 'https://slack.com/api/test'], deps);
884
+ expect(exitCode).toBe(0);
885
+ expect(capturedArgs).toEqual(['https://slack.com/api/test']);
886
+ expect(errorLogs).toHaveLength(0);
887
+ });
888
+ it('should still inject credentials for known services when passthroughUnknown is enabled', async () => {
889
+ const storePath = join(tempDir, 'credentials.json');
890
+ await writeSecureFile(storePath, JSON.stringify({
891
+ slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
892
+ }));
893
+ const deps = createMockDependencies({
894
+ config: createMockConfig({ passthroughUnknown: true }),
895
+ });
896
+ await runCommand(['curl', 'https://slack.com/api/test'], deps);
897
+ expect(exitCode).toBe(0);
898
+ expect(capturedArgs).toContain('Authorization: Bearer stored-token');
899
+ });
852
900
  it('should read credentials from store and not call login', async () => {
853
901
  const storePath = join(tempDir, 'credentials.json');
854
902
  await writeSecureFile(storePath, JSON.stringify({
@@ -872,7 +920,7 @@ describe('CLI commands with dependency injection', () => {
872
920
  getSession: vi.fn().mockReturnValue({ login: mockLogin }),
873
921
  };
874
922
  const deps = createMockDependencies({
875
- registry: new Registry([mockSlackService]),
923
+ registry: new ServiceRegistry([mockSlackService]),
876
924
  });
877
925
  await runCommand(['curl', 'https://slack.com/api/test'], deps);
878
926
  expect(mockLogin).not.toHaveBeenCalled();
@@ -891,7 +939,7 @@ describe('CLI commands with dependency injection', () => {
891
939
  telegram: { objectType: 'telegramBot', token: '123456:ABC-DEF' },
892
940
  }));
893
941
  const deps = createMockDependencies({
894
- registry: new Registry([TELEGRAM]),
942
+ registry: new ServiceRegistry([TELEGRAM]),
895
943
  });
896
944
  await runCommand(['curl', 'https://api.telegram.org/getMe'], deps);
897
945
  expect(capturedArgs).toEqual(['https://api.telegram.org/bot123456:ABC-DEF/getMe']);
@@ -909,7 +957,7 @@ describe('CLI commands with dependency injection', () => {
909
957
  loginUrl: 'https://nologin.example.com',
910
958
  info: 'A service without browser login support.',
911
959
  credentialCheckCurlArguments: [],
912
- checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
960
+ checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Valid),
913
961
  setCredentialsExample(serviceName) {
914
962
  return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
915
963
  },
@@ -919,7 +967,7 @@ describe('CLI commands with dependency injection', () => {
919
967
  // No getSession - service doesn't support browser login
920
968
  };
921
969
  const deps = createMockDependencies({
922
- registry: new Registry([noLoginService]),
970
+ registry: new ServiceRegistry([noLoginService]),
923
971
  });
924
972
  await runCommand(['curl', 'https://nologin.example.com/api/test'], deps);
925
973
  expect(exitCode).toBe(0);
@@ -986,7 +1034,7 @@ describe('CLI commands with dependency injection', () => {
986
1034
  // No getSession - service doesn't support browser login
987
1035
  };
988
1036
  const deps = createMockDependencies({
989
- registry: new Registry([noLoginService]),
1037
+ registry: new ServiceRegistry([noLoginService]),
990
1038
  });
991
1039
  await runCommand(['auth', 'browser', 'nologin'], deps);
992
1040
  expect(exitCode).toBe(1);
@@ -1043,7 +1091,7 @@ describe('CLI commands with dependency injection', () => {
1043
1091
  // No getSession - service doesn't support browser login
1044
1092
  };
1045
1093
  const deps = createMockDependencies({
1046
- registry: new Registry([nocurlService]),
1094
+ registry: new ServiceRegistry([nocurlService]),
1047
1095
  });
1048
1096
  await runCommand(['auth', 'browser', 'nocurl-only'], deps);
1049
1097
  expect(exitCode).toBe(1);
@@ -1052,7 +1100,7 @@ describe('CLI commands with dependency injection', () => {
1052
1100
  describe('services register command', () => {
1053
1101
  it('should register a new service', async () => {
1054
1102
  const deps = createMockDependencies({
1055
- registry: new Registry([GITLAB]),
1103
+ registry: new ServiceRegistry([GITLAB]),
1056
1104
  });
1057
1105
  await runCommand([
1058
1106
  'services',
@@ -1072,7 +1120,7 @@ describe('CLI commands with dependency injection', () => {
1072
1120
  });
1073
1121
  it('should persist registration to config.json', async () => {
1074
1122
  const deps = createMockDependencies({
1075
- registry: new Registry([GITLAB]),
1123
+ registry: new ServiceRegistry([GITLAB]),
1076
1124
  });
1077
1125
  await runCommand([
1078
1126
  'services',
@@ -1092,7 +1140,7 @@ describe('CLI commands with dependency injection', () => {
1092
1140
  });
1093
1141
  it('should reject unknown service family', async () => {
1094
1142
  const deps = createMockDependencies({
1095
- registry: new Registry([GITLAB]),
1143
+ registry: new ServiceRegistry([GITLAB]),
1096
1144
  });
1097
1145
  await runCommand([
1098
1146
  'services',
@@ -1108,7 +1156,7 @@ describe('CLI commands with dependency injection', () => {
1108
1156
  });
1109
1157
  it('should reject duplicate service name', async () => {
1110
1158
  const deps = createMockDependencies({
1111
- registry: new Registry([GITLAB]),
1159
+ registry: new ServiceRegistry([GITLAB]),
1112
1160
  });
1113
1161
  await runCommand([
1114
1162
  'services',
@@ -1124,7 +1172,7 @@ describe('CLI commands with dependency injection', () => {
1124
1172
  });
1125
1173
  it('should canonicalize service name to lowercase', async () => {
1126
1174
  const deps = createMockDependencies({
1127
- registry: new Registry([GITLAB]),
1175
+ registry: new ServiceRegistry([GITLAB]),
1128
1176
  });
1129
1177
  await runCommand([
1130
1178
  'services',
@@ -1141,7 +1189,7 @@ describe('CLI commands with dependency injection', () => {
1141
1189
  });
1142
1190
  it('should convert spaces to hyphens in service name', async () => {
1143
1191
  const deps = createMockDependencies({
1144
- registry: new Registry([GITLAB]),
1192
+ registry: new ServiceRegistry([GITLAB]),
1145
1193
  });
1146
1194
  await runCommand(['services', 'register', 'my api', '--base-api-url', 'https://api.example.com/'], deps);
1147
1195
  expect(exitCode).toBeNull();
@@ -1150,7 +1198,7 @@ describe('CLI commands with dependency injection', () => {
1150
1198
  });
1151
1199
  it('should reject service name with invalid characters', async () => {
1152
1200
  const deps = createMockDependencies({
1153
- registry: new Registry([GITLAB]),
1201
+ registry: new ServiceRegistry([GITLAB]),
1154
1202
  });
1155
1203
  await runCommand(['services', 'register', 'my@service!', '--base-api-url', 'https://api.example.com/'], deps);
1156
1204
  expect(exitCode).toBe(1);
@@ -1160,7 +1208,7 @@ describe('CLI commands with dependency injection', () => {
1160
1208
  const storePath = join(tempDir, 'credentials.json');
1161
1209
  await writeSecureFile(storePath, '{}');
1162
1210
  const deps = createMockDependencies({
1163
- registry: new Registry([GITLAB]),
1211
+ registry: new ServiceRegistry([GITLAB]),
1164
1212
  });
1165
1213
  await runCommand([
1166
1214
  'services',
@@ -1179,7 +1227,7 @@ describe('CLI commands with dependency injection', () => {
1179
1227
  });
1180
1228
  it('should persist and restore loginUrl', async () => {
1181
1229
  const deps = createMockDependencies({
1182
- registry: new Registry([GITHUB]),
1230
+ registry: new ServiceRegistry([GITHUB]),
1183
1231
  });
1184
1232
  await runCommand([
1185
1233
  'services',
@@ -1197,7 +1245,7 @@ describe('CLI commands with dependency injection', () => {
1197
1245
  });
1198
1246
  it('should reject --login-url without --service-family', async () => {
1199
1247
  const deps = createMockDependencies({
1200
- registry: new Registry([]),
1248
+ registry: new ServiceRegistry([]),
1201
1249
  });
1202
1250
  await runCommand([
1203
1251
  'services',
@@ -1213,7 +1261,7 @@ describe('CLI commands with dependency injection', () => {
1213
1261
  });
1214
1262
  it('should reject --login-url when service family does not support browser login', async () => {
1215
1263
  const deps = createMockDependencies({
1216
- registry: new Registry([GITLAB]),
1264
+ registry: new ServiceRegistry([GITLAB]),
1217
1265
  });
1218
1266
  await runCommand([
1219
1267
  'services',
@@ -1231,7 +1279,7 @@ describe('CLI commands with dependency injection', () => {
1231
1279
  });
1232
1280
  it('should require --login-url when service family supports browser login', async () => {
1233
1281
  const deps = createMockDependencies({
1234
- registry: new Registry([GITHUB]),
1282
+ registry: new ServiceRegistry([GITHUB]),
1235
1283
  });
1236
1284
  await runCommand([
1237
1285
  'services',
@@ -1247,7 +1295,7 @@ describe('CLI commands with dependency injection', () => {
1247
1295
  });
1248
1296
  it('should make registered service usable with auth set', async () => {
1249
1297
  const deps = createMockDependencies({
1250
- registry: new Registry([GITLAB]),
1298
+ registry: new ServiceRegistry([GITLAB]),
1251
1299
  });
1252
1300
  // Register the service
1253
1301
  await runCommand([
@@ -1270,7 +1318,7 @@ describe('CLI commands with dependency injection', () => {
1270
1318
  });
1271
1319
  it('should register a service without --service-family', async () => {
1272
1320
  const deps = createMockDependencies({
1273
- registry: new Registry([GITLAB]),
1321
+ registry: new ServiceRegistry([GITLAB]),
1274
1322
  });
1275
1323
  await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
1276
1324
  expect(exitCode).toBeNull();
@@ -1282,7 +1330,7 @@ describe('CLI commands with dependency injection', () => {
1282
1330
  });
1283
1331
  it('should persist registration without service family to config.json', async () => {
1284
1332
  const deps = createMockDependencies({
1285
- registry: new Registry([GITLAB]),
1333
+ registry: new ServiceRegistry([GITLAB]),
1286
1334
  });
1287
1335
  await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
1288
1336
  const configPath = deps.config.configPath;
@@ -1295,7 +1343,7 @@ describe('CLI commands with dependency injection', () => {
1295
1343
  const storePath = join(tempDir, 'credentials.json');
1296
1344
  await writeSecureFile(storePath, '{}');
1297
1345
  const deps = createMockDependencies({
1298
- registry: new Registry([GITLAB]),
1346
+ registry: new ServiceRegistry([GITLAB]),
1299
1347
  });
1300
1348
  await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
1301
1349
  logs = [];
@@ -1306,7 +1354,7 @@ describe('CLI commands with dependency injection', () => {
1306
1354
  });
1307
1355
  it('should make service without family usable with auth set and curl', async () => {
1308
1356
  const deps = createMockDependencies({
1309
- registry: new Registry([GITLAB]),
1357
+ registry: new ServiceRegistry([GITLAB]),
1310
1358
  });
1311
1359
  // Register the service without family
1312
1360
  await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
@@ -1328,7 +1376,7 @@ describe('CLI commands with dependency injection', () => {
1328
1376
  });
1329
1377
  it('should reject browser login for service without family', async () => {
1330
1378
  const deps = createMockDependencies({
1331
- registry: new Registry([GITLAB]),
1379
+ registry: new ServiceRegistry([GITLAB]),
1332
1380
  });
1333
1381
  await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
1334
1382
  logs = [];
@@ -1340,7 +1388,7 @@ describe('CLI commands with dependency injection', () => {
1340
1388
  });
1341
1389
  it('should reject set-nocurl for service without family', async () => {
1342
1390
  const deps = createMockDependencies({
1343
- registry: new Registry([GITLAB]),
1391
+ registry: new ServiceRegistry([GITLAB]),
1344
1392
  });
1345
1393
  await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
1346
1394
  logs = [];
@@ -1352,7 +1400,7 @@ describe('CLI commands with dependency injection', () => {
1352
1400
  });
1353
1401
  it('should make registered service usable with curl', async () => {
1354
1402
  const deps = createMockDependencies({
1355
- registry: new Registry([GITLAB]),
1403
+ registry: new ServiceRegistry([GITLAB]),
1356
1404
  });
1357
1405
  // Register the service
1358
1406
  await runCommand([
@@ -1384,7 +1432,7 @@ describe('CLI commands with dependency injection', () => {
1384
1432
  describe('services deregister command', () => {
1385
1433
  it('should deregister a registered service', async () => {
1386
1434
  const deps = createMockDependencies({
1387
- registry: new Registry([GITLAB]),
1435
+ registry: new ServiceRegistry([GITLAB]),
1388
1436
  });
1389
1437
  // Register a service first
1390
1438
  await runCommand([
@@ -1404,7 +1452,7 @@ describe('CLI commands with dependency injection', () => {
1404
1452
  });
1405
1453
  it('should remove service from config.json', async () => {
1406
1454
  const deps = createMockDependencies({
1407
- registry: new Registry([GITLAB]),
1455
+ registry: new ServiceRegistry([GITLAB]),
1408
1456
  });
1409
1457
  await runCommand([
1410
1458
  'services',
@@ -1435,7 +1483,7 @@ describe('CLI commands with dependency injection', () => {
1435
1483
  });
1436
1484
  it('should reject deregistering when credentials still exist', async () => {
1437
1485
  const deps = createMockDependencies({
1438
- registry: new Registry([GITLAB]),
1486
+ registry: new ServiceRegistry([GITLAB]),
1439
1487
  });
1440
1488
  // Register a service
1441
1489
  await runCommand([
@@ -1468,7 +1516,7 @@ describe('CLI commands with dependency injection', () => {
1468
1516
  });
1469
1517
  it('should allow deregistering after credentials are cleared', async () => {
1470
1518
  const deps = createMockDependencies({
1471
- registry: new Registry([GITLAB]),
1519
+ registry: new ServiceRegistry([GITLAB]),
1472
1520
  });
1473
1521
  // Register
1474
1522
  await runCommand([
@@ -1501,6 +1549,143 @@ describe('CLI commands with dependency injection', () => {
1501
1549
  expect(logs).toContain("Service 'my-gitlab' deregistered.");
1502
1550
  });
1503
1551
  });
1552
+ describe('gateway mode (LATCHKEY_GATEWAY)', () => {
1553
+ const GATEWAY_URL = 'http://localhost:9000';
1554
+ const originalFetch = globalThis.fetch;
1555
+ afterEach(() => {
1556
+ globalThis.fetch = originalFetch;
1557
+ });
1558
+ function makeFetchMock(response) {
1559
+ const fetchMock = vi.fn().mockResolvedValue(response);
1560
+ globalThis.fetch = fetchMock;
1561
+ return fetchMock;
1562
+ }
1563
+ it('forwards `services list` to the gateway /latchkey endpoint', async () => {
1564
+ const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: ['slack', 'github'] }), { status: 200 }));
1565
+ const deps = createMockDependencies({
1566
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1567
+ });
1568
+ await runCommand(['services', 'list', '--builtin'], deps);
1569
+ expect(fetchMock).toHaveBeenCalledTimes(1);
1570
+ const [url, init] = fetchMock.mock.calls[0];
1571
+ expect(url).toBe(`${GATEWAY_URL}/latchkey`);
1572
+ expect(init.method).toBe('POST');
1573
+ expect(JSON.parse(init.body)).toEqual({
1574
+ command: 'services list',
1575
+ params: { builtin: true },
1576
+ });
1577
+ expect(logs).toHaveLength(1);
1578
+ expect(JSON.parse(logs[0] ?? '')).toEqual(['slack', 'github']);
1579
+ });
1580
+ it('forwards `services info` to the gateway /latchkey endpoint', async () => {
1581
+ const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: { type: 'built-in' } }), { status: 200 }));
1582
+ const deps = createMockDependencies({
1583
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1584
+ });
1585
+ await runCommand(['services', 'info', 'slack'], deps);
1586
+ expect(fetchMock).toHaveBeenCalledTimes(1);
1587
+ const [, init] = fetchMock.mock.calls[0];
1588
+ expect(JSON.parse(init.body)).toEqual({
1589
+ command: 'services info',
1590
+ params: { serviceName: 'slack' },
1591
+ });
1592
+ });
1593
+ it('forwards `auth list` to the gateway /latchkey endpoint', async () => {
1594
+ const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: { slack: { credentialType: 'slack' } } }), {
1595
+ status: 200,
1596
+ }));
1597
+ const deps = createMockDependencies({
1598
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1599
+ });
1600
+ await runCommand(['auth', 'list'], deps);
1601
+ const [, init] = fetchMock.mock.calls[0];
1602
+ expect(JSON.parse(init.body)).toEqual({ command: 'auth list' });
1603
+ expect(JSON.parse(logs[0] ?? '')).toEqual({
1604
+ slack: { credentialType: 'slack' },
1605
+ });
1606
+ });
1607
+ it('forwards `auth browser` to the gateway /latchkey endpoint', async () => {
1608
+ const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: null }), { status: 200 }));
1609
+ const deps = createMockDependencies({
1610
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1611
+ });
1612
+ await runCommand(['auth', 'browser', 'slack'], deps);
1613
+ const [, init] = fetchMock.mock.calls[0];
1614
+ expect(JSON.parse(init.body)).toEqual({
1615
+ command: 'auth browser',
1616
+ params: { serviceName: 'slack' },
1617
+ });
1618
+ expect(logs).toContain('Done');
1619
+ });
1620
+ it('forwards `auth browser-prepare` and reports `Already prepared.` when the gateway says so', async () => {
1621
+ makeFetchMock(new Response(JSON.stringify({ result: { alreadyPrepared: true } }), { status: 200 }));
1622
+ const deps = createMockDependencies({
1623
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1624
+ });
1625
+ await runCommand(['auth', 'browser-prepare', 'slack'], deps);
1626
+ expect(logs).toContain('Already prepared.');
1627
+ });
1628
+ it.each([
1629
+ ['services list', ['services', 'list']],
1630
+ ['services info', ['services', 'info', 'foo']],
1631
+ ['auth list', ['auth', 'list']],
1632
+ ['auth browser', ['auth', 'browser', 'slack']],
1633
+ ['auth browser-prepare', ['auth', 'browser-prepare', 'slack']],
1634
+ ])('reports gateway errors on stderr (not stdout) for `%s`', async (_name, argv) => {
1635
+ makeFetchMock(new Response(JSON.stringify({ error: 'Unknown service: foo.' }), { status: 400 }));
1636
+ const deps = createMockDependencies({
1637
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1638
+ });
1639
+ await runCommand(argv, deps);
1640
+ expect(exitCode).toBe(1);
1641
+ expect(errorLogs.some((message) => message.includes('Unknown service: foo.'))).toBe(true);
1642
+ // The error message must never be printed to stdout — that would
1643
+ // interleave into JSON output consumed by callers.
1644
+ expect(logs.join('\n')).not.toContain('Unknown service: foo.');
1645
+ expect(logs).toEqual([]);
1646
+ });
1647
+ it('rewrites the curl target URL to the gateway /gateway endpoint', async () => {
1648
+ const fetchMock = vi.fn();
1649
+ globalThis.fetch = fetchMock;
1650
+ const deps = createMockDependencies({
1651
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1652
+ });
1653
+ await runCommand(['curl', '-X', 'GET', 'https://slack.com/api/auth.test'], deps);
1654
+ expect(fetchMock).not.toHaveBeenCalled();
1655
+ expect(capturedArgs).toEqual([
1656
+ '-X',
1657
+ 'GET',
1658
+ `${GATEWAY_URL}/gateway/https://slack.com/api/auth.test`,
1659
+ ]);
1660
+ });
1661
+ it('errors when `latchkey curl` has no URL to rewrite', async () => {
1662
+ const deps = createMockDependencies({
1663
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1664
+ });
1665
+ await runCommand(['curl', '-X', 'GET'], deps);
1666
+ expect(exitCode).toBe(1);
1667
+ expect(capturedArgs).toEqual([]);
1668
+ });
1669
+ it.each([
1670
+ [
1671
+ 'services register',
1672
+ ['services', 'register', 'my-gitlab', '--base-api-url', 'https://gitlab.example.com'],
1673
+ ],
1674
+ ['services deregister', ['services', 'deregister', 'my-gitlab']],
1675
+ ['auth clear', ['auth', 'clear', 'slack']],
1676
+ ['auth set', ['auth', 'set', 'slack', '-H', 'Authorization: Bearer xoxb-test']],
1677
+ ['auth set-nocurl', ['auth', 'set-nocurl', 'slack', 'x']],
1678
+ ['gateway', ['gateway']],
1679
+ ['ensure-browser', ['ensure-browser']],
1680
+ ])('refuses to run `%s` in gateway mode', async (_name, argv) => {
1681
+ const deps = createMockDependencies({
1682
+ config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
1683
+ });
1684
+ await runCommand(argv, deps);
1685
+ expect(exitCode).toBe(1);
1686
+ expect(errorLogs.some((message) => message.includes('LATCHKEY_GATEWAY'))).toBe(true);
1687
+ });
1688
+ });
1504
1689
  });
1505
1690
  describe('registeredServiceStore', () => {
1506
1691
  let tempDir;
@@ -1545,8 +1730,8 @@ describe('registeredServiceStore', () => {
1545
1730
  baseApiUrl: 'https://gitlab.mycompany.com/api/',
1546
1731
  serviceFamily: 'gitlab',
1547
1732
  });
1548
- const registry = new Registry([GITLAB]);
1549
- loadRegisteredServicesIntoRegistry(configPath, registry);
1733
+ const registry = new ServiceRegistry([GITLAB]);
1734
+ loadRegisteredServicesIntoServiceRegistry(configPath, registry);
1550
1735
  const service = registry.getByName('my-gitlab');
1551
1736
  expect(service).not.toBeNull();
1552
1737
  expect(service.baseApiUrls).toEqual(['https://gitlab.mycompany.com/api/']);
@@ -1558,8 +1743,8 @@ describe('registeredServiceStore', () => {
1558
1743
  serviceFamily: 'gitlab',
1559
1744
  loginUrl: 'https://gitlab.mycompany.com/users/sign_in',
1560
1745
  });
1561
- const registry = new Registry([GITLAB]);
1562
- loadRegisteredServicesIntoRegistry(configPath, registry);
1746
+ const registry = new ServiceRegistry([GITLAB]);
1747
+ loadRegisteredServicesIntoServiceRegistry(configPath, registry);
1563
1748
  const service = registry.getByName('my-gitlab');
1564
1749
  expect(service).not.toBeNull();
1565
1750
  expect(service.loginUrl).toBe('https://gitlab.mycompany.com/users/sign_in');
@@ -1569,8 +1754,8 @@ describe('registeredServiceStore', () => {
1569
1754
  saveRegisteredService(configPath, 'my-api', {
1570
1755
  baseApiUrl: 'https://api.example.com/',
1571
1756
  });
1572
- const registry = new Registry([GITLAB]);
1573
- loadRegisteredServicesIntoRegistry(configPath, registry);
1757
+ const registry = new ServiceRegistry([GITLAB]);
1758
+ loadRegisteredServicesIntoServiceRegistry(configPath, registry);
1574
1759
  const service = registry.getByName('my-api');
1575
1760
  expect(service).not.toBeNull();
1576
1761
  expect(service.baseApiUrls).toEqual(['https://api.example.com/']);
@@ -1583,8 +1768,8 @@ describe('registeredServiceStore', () => {
1583
1768
  baseApiUrl: 'https://unknown.example.com/api/',
1584
1769
  serviceFamily: 'nonexistent',
1585
1770
  });
1586
- const registry = new Registry([GITLAB]);
1587
- loadRegisteredServicesIntoRegistry(configPath, registry);
1771
+ const registry = new ServiceRegistry([GITLAB]);
1772
+ loadRegisteredServicesIntoServiceRegistry(configPath, registry);
1588
1773
  expect(registry.getByName('my-unknown')).toBeNull();
1589
1774
  });
1590
1775
  it('should delete a registered service from config', () => {