latchkey 3.14.0 → 3.16.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 (156) hide show
  1. package/README.md +10 -0
  2. package/dist/src/apiCredentials/base.d.ts +53 -5
  3. package/dist/src/apiCredentials/base.d.ts.map +1 -1
  4. package/dist/src/apiCredentials/base.js +43 -9
  5. package/dist/src/apiCredentials/base.js.map +1 -1
  6. package/dist/src/apiCredentials/serialization.d.ts +11 -139
  7. package/dist/src/apiCredentials/serialization.d.ts.map +1 -1
  8. package/dist/src/apiCredentials/serialization.js +53 -81
  9. package/dist/src/apiCredentials/serialization.js.map +1 -1
  10. package/dist/src/apiCredentials/store.d.ts +7 -2
  11. package/dist/src/apiCredentials/store.d.ts.map +1 -1
  12. package/dist/src/apiCredentials/store.js +18 -8
  13. package/dist/src/apiCredentials/store.js.map +1 -1
  14. package/dist/src/browserStateImport.d.ts +50 -0
  15. package/dist/src/browserStateImport.d.ts.map +1 -0
  16. package/dist/src/browserStateImport.js +260 -0
  17. package/dist/src/browserStateImport.js.map +1 -0
  18. package/dist/src/cli.js +13 -2
  19. package/dist/src/cli.js.map +1 -1
  20. package/dist/src/cliCommands.d.ts +15 -7
  21. package/dist/src/cliCommands.d.ts.map +1 -1
  22. package/dist/src/cliCommands.js +64 -16
  23. package/dist/src/cliCommands.js.map +1 -1
  24. package/dist/src/config.d.ts +10 -0
  25. package/dist/src/config.d.ts.map +1 -1
  26. package/dist/src/config.js +15 -0
  27. package/dist/src/config.js.map +1 -1
  28. package/dist/src/configDataStore.d.ts +3 -0
  29. package/dist/src/configDataStore.d.ts.map +1 -1
  30. package/dist/src/configDataStore.js +1 -0
  31. package/dist/src/configDataStore.js.map +1 -1
  32. package/dist/src/curlInjection.d.ts +2 -0
  33. package/dist/src/curlInjection.d.ts.map +1 -1
  34. package/dist/src/curlInjection.js +12 -6
  35. package/dist/src/curlInjection.js.map +1 -1
  36. package/dist/src/diagnosticHeaders.d.ts +19 -0
  37. package/dist/src/diagnosticHeaders.d.ts.map +1 -0
  38. package/dist/src/diagnosticHeaders.js +20 -0
  39. package/dist/src/diagnosticHeaders.js.map +1 -0
  40. package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
  41. package/dist/src/gateway/gatewayEndpoint.js +1 -0
  42. package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
  43. package/dist/src/gateway/server.js +1 -1
  44. package/dist/src/gateway/server.js.map +1 -1
  45. package/dist/src/index.d.ts +3 -3
  46. package/dist/src/index.d.ts.map +1 -1
  47. package/dist/src/index.js +2 -2
  48. package/dist/src/index.js.map +1 -1
  49. package/dist/src/migrations.d.ts.map +1 -1
  50. package/dist/src/migrations.js +8 -8
  51. package/dist/src/migrations.js.map +1 -1
  52. package/dist/src/oauthUtils.d.ts +17 -0
  53. package/dist/src/oauthUtils.d.ts.map +1 -1
  54. package/dist/src/oauthUtils.js +22 -1
  55. package/dist/src/oauthUtils.js.map +1 -1
  56. package/dist/src/pluginSdk.d.ts +118 -0
  57. package/dist/src/pluginSdk.d.ts.map +1 -0
  58. package/dist/src/pluginSdk.js +98 -0
  59. package/dist/src/pluginSdk.js.map +1 -0
  60. package/dist/src/plugins.d.ts +74 -0
  61. package/dist/src/plugins.d.ts.map +1 -0
  62. package/dist/src/plugins.js +221 -0
  63. package/dist/src/plugins.js.map +1 -0
  64. package/dist/src/serviceRegistry.d.ts +2 -2
  65. package/dist/src/serviceRegistry.d.ts.map +1 -1
  66. package/dist/src/serviceRegistry.js +3 -3
  67. package/dist/src/serviceRegistry.js.map +1 -1
  68. package/dist/src/services/aws.d.ts +2 -1
  69. package/dist/src/services/aws.d.ts.map +1 -1
  70. package/dist/src/services/aws.js +4 -2
  71. package/dist/src/services/aws.js.map +1 -1
  72. package/dist/src/services/core/base.d.ts +53 -3
  73. package/dist/src/services/core/base.d.ts.map +1 -1
  74. package/dist/src/services/core/base.js +63 -3
  75. package/dist/src/services/core/base.js.map +1 -1
  76. package/dist/src/services/dropbox.d.ts.map +1 -1
  77. package/dist/src/services/dropbox.js +2 -2
  78. package/dist/src/services/dropbox.js.map +1 -1
  79. package/dist/src/services/fastmail.d.ts +4 -19
  80. package/dist/src/services/fastmail.d.ts.map +1 -1
  81. package/dist/src/services/fastmail.js +17 -22
  82. package/dist/src/services/fastmail.js.map +1 -1
  83. package/dist/src/services/google/base.d.ts +8 -3
  84. package/dist/src/services/google/base.d.ts.map +1 -1
  85. package/dist/src/services/google/base.js +23 -13
  86. package/dist/src/services/google/base.js.map +1 -1
  87. package/dist/src/services/index.d.ts +1 -1
  88. package/dist/src/services/index.d.ts.map +1 -1
  89. package/dist/src/services/index.js +1 -1
  90. package/dist/src/services/index.js.map +1 -1
  91. package/dist/src/services/notion-mcp.d.ts +16 -16
  92. package/dist/src/services/notion-mcp.d.ts.map +1 -1
  93. package/dist/src/services/notion-mcp.js +36 -21
  94. package/dist/src/services/notion-mcp.js.map +1 -1
  95. package/dist/src/services/ramp.d.ts.map +1 -1
  96. package/dist/src/services/ramp.js +2 -2
  97. package/dist/src/services/ramp.js.map +1 -1
  98. package/dist/src/services/slack.d.ts +2 -1
  99. package/dist/src/services/slack.d.ts.map +1 -1
  100. package/dist/src/services/slack.js +4 -2
  101. package/dist/src/services/slack.js.map +1 -1
  102. package/dist/src/services/tailscale.d.ts +2 -1
  103. package/dist/src/services/tailscale.d.ts.map +1 -1
  104. package/dist/src/services/tailscale.js +4 -2
  105. package/dist/src/services/tailscale.js.map +1 -1
  106. package/dist/src/services/telegram.d.ts +2 -1
  107. package/dist/src/services/telegram.d.ts.map +1 -1
  108. package/dist/src/services/telegram.js +4 -2
  109. package/dist/src/services/telegram.js.map +1 -1
  110. package/dist/src/services/zoom.d.ts +7 -5
  111. package/dist/src/services/zoom.d.ts.map +1 -1
  112. package/dist/src/services/zoom.js +11 -8
  113. package/dist/src/services/zoom.js.map +1 -1
  114. package/dist/src/version.d.ts +1 -1
  115. package/dist/src/version.js +1 -1
  116. package/dist/tests/apiCredentialStore.test.js +54 -1
  117. package/dist/tests/apiCredentialStore.test.js.map +1 -1
  118. package/dist/tests/apiCredentials.test.js +73 -12
  119. package/dist/tests/apiCredentials.test.js.map +1 -1
  120. package/dist/tests/browserStateImport.test.d.ts +2 -0
  121. package/dist/tests/browserStateImport.test.d.ts.map +1 -0
  122. package/dist/tests/browserStateImport.test.js +88 -0
  123. package/dist/tests/browserStateImport.test.js.map +1 -0
  124. package/dist/tests/cli.test.js +33 -1
  125. package/dist/tests/cli.test.js.map +1 -1
  126. package/dist/tests/config.test.js +7 -0
  127. package/dist/tests/config.test.js.map +1 -1
  128. package/dist/tests/diagnosticHeaders.test.d.ts +2 -0
  129. package/dist/tests/diagnosticHeaders.test.d.ts.map +1 -0
  130. package/dist/tests/diagnosticHeaders.test.js +22 -0
  131. package/dist/tests/diagnosticHeaders.test.js.map +1 -0
  132. package/dist/tests/followupFailureRecovery.test.d.ts +11 -0
  133. package/dist/tests/followupFailureRecovery.test.d.ts.map +1 -0
  134. package/dist/tests/followupFailureRecovery.test.js +101 -0
  135. package/dist/tests/followupFailureRecovery.test.js.map +1 -0
  136. package/dist/tests/gateway.test.js +25 -4
  137. package/dist/tests/gateway.test.js.map +1 -1
  138. package/dist/tests/gatewayExtensions.test.js +5 -2
  139. package/dist/tests/gatewayExtensions.test.js.map +1 -1
  140. package/dist/tests/latchkeyEndpoint.test.js +8 -6
  141. package/dist/tests/latchkeyEndpoint.test.js.map +1 -1
  142. package/dist/tests/notion-mcp.test.d.ts +2 -0
  143. package/dist/tests/notion-mcp.test.d.ts.map +1 -0
  144. package/dist/tests/notion-mcp.test.js +16 -0
  145. package/dist/tests/notion-mcp.test.js.map +1 -0
  146. package/dist/tests/oauthUtils.test.js +23 -1
  147. package/dist/tests/oauthUtils.test.js.map +1 -1
  148. package/dist/tests/plugins.test.d.ts +2 -0
  149. package/dist/tests/plugins.test.d.ts.map +1 -0
  150. package/dist/tests/plugins.test.js +506 -0
  151. package/dist/tests/plugins.test.js.map +1 -0
  152. package/dist/tests/sharedOperations.test.js +63 -2
  153. package/dist/tests/sharedOperations.test.js.map +1 -1
  154. package/dist/tests/tailscale.test.js +1 -3
  155. package/dist/tests/tailscale.test.js.map +1 -1
  156. package/package.json +15 -1
@@ -0,0 +1,506 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { PluginLoadError, combineWithPluginApiCredentialsTypes, combineWithPluginServices, isLatchkeyVersionSupported, loadPlugins, } from '../src/plugins.js';
8
+ import { createLatchkeySdk } from '../src/pluginSdk.js';
9
+ import { AuthorizationBearer } from '../src/apiCredentials/base.js';
10
+ import { BUILTIN_API_CREDENTIALS_TYPES } from '../src/apiCredentials/serialization.js';
11
+ import { Service } from '../src/services/core/base.js';
12
+ import { SLACK } from '../src/services/index.js';
13
+ import { VERSION } from '../src/version.js';
14
+ const SDK_VERSION = '3.15.2';
15
+ const SDK = createLatchkeySdk(SDK_VERSION);
16
+ const SUPPORTED_VERSION_RANGE = '^3.15.0';
17
+ function pluginSource(serviceNames, options = {}) {
18
+ const latchkeyVersion = JSON.stringify(options.latchkeyVersion ?? SUPPORTED_VERSION_RANGE);
19
+ const services = serviceNames.map((name) => `new PluginService(${JSON.stringify(name)})`);
20
+ const factory = options.asyncFactory === true ? 'async (sdk)' : '(sdk)';
21
+ return `
22
+ export default ${factory} => {
23
+ class PluginService extends sdk.Service {
24
+ constructor(name) {
25
+ super();
26
+ this.name = name;
27
+ this.displayName = name;
28
+ this.baseApiUrls = [\`https://\${name}.example.com/\`];
29
+ this.loginUrl = '';
30
+ this.info = \`Plugin service \${name}, loaded by Latchkey \${sdk.latchkeyVersion}.\`;
31
+ this.credentialCheckCurlArguments = [];
32
+ }
33
+ setCredentialsExample(serviceName) {
34
+ return \`latchkey auth set \${serviceName} -H "Authorization: Bearer <token>"\`;
35
+ }
36
+ }
37
+ return {
38
+ latchkeyVersion: ${latchkeyVersion},
39
+ services: [${services.join(', ')}],
40
+ ${options.apiCredentialsTypes === undefined ? '' : `apiCredentialsTypes: ${options.apiCredentialsTypes},`}
41
+ };
42
+ };
43
+ `;
44
+ }
45
+ function writePlugin(pluginsDirectory, pluginName, files) {
46
+ const pluginDirectory = join(pluginsDirectory, pluginName);
47
+ for (const [relativePath, content] of Object.entries(files)) {
48
+ const filePath = join(pluginDirectory, relativePath);
49
+ mkdirSync(dirname(filePath), { recursive: true });
50
+ writeFileSync(filePath, content, 'utf-8');
51
+ }
52
+ return pluginDirectory;
53
+ }
54
+ function serviceNames(plugin) {
55
+ return plugin.services.map((service) => service.name);
56
+ }
57
+ async function expectLoadError(pluginsDirectory, messagePart) {
58
+ const loading = loadPlugins(pluginsDirectory, SDK);
59
+ await expect(loading).rejects.toBeInstanceOf(PluginLoadError);
60
+ await expect(loading).rejects.toThrow(messagePart);
61
+ }
62
+ describe('loadPlugins', () => {
63
+ let tempDir;
64
+ let pluginsDirectory;
65
+ beforeEach(() => {
66
+ tempDir = mkdtempSync(join(tmpdir(), 'latchkey-plugins-test-'));
67
+ pluginsDirectory = join(tempDir, 'plugins');
68
+ mkdirSync(pluginsDirectory);
69
+ });
70
+ afterEach(() => {
71
+ rmSync(tempDir, { recursive: true, force: true });
72
+ });
73
+ it('returns no plugins when the directory does not exist', async () => {
74
+ expect(await loadPlugins(join(tempDir, 'nonexistent'), SDK)).toEqual([]);
75
+ });
76
+ it('returns no plugins when the path is a file', async () => {
77
+ const filePath = join(tempDir, 'not-a-directory');
78
+ writeFileSync(filePath, '', 'utf-8');
79
+ expect(await loadPlugins(filePath, SDK)).toEqual([]);
80
+ });
81
+ it('loads a plugin from its index.js and hands its factory the sdk', async () => {
82
+ const pluginDirectory = writePlugin(pluginsDirectory, 'foo', {
83
+ 'index.js': pluginSource(['foo', 'foo-admin']),
84
+ });
85
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
86
+ expect(plugins).toHaveLength(1);
87
+ expect(plugins[0].name).toBe('foo');
88
+ expect(plugins[0].directory).toBe(pluginDirectory);
89
+ expect(serviceNames(plugins[0])).toEqual(['foo', 'foo-admin']);
90
+ expect(plugins[0].services[0]).toBeInstanceOf(Service);
91
+ expect(plugins[0].services[0].info).toContain(`loaded by Latchkey ${SDK_VERSION}`);
92
+ });
93
+ it('loads plugins in alphabetical order and ignores what is not a plugin directory', async () => {
94
+ writePlugin(pluginsDirectory, 'zeta', { 'index.js': pluginSource(['zeta']) });
95
+ writePlugin(pluginsDirectory, 'alpha', { 'index.js': pluginSource(['alpha']) });
96
+ writePlugin(pluginsDirectory, '.hidden', { 'index.js': pluginSource(['hidden']) });
97
+ writePlugin(pluginsDirectory, 'node_modules', { 'index.js': pluginSource(['dependency']) });
98
+ writeFileSync(join(pluginsDirectory, 'stray.js'), pluginSource(['stray']), 'utf-8');
99
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
100
+ expect(plugins.map((plugin) => plugin.name)).toEqual(['alpha', 'zeta']);
101
+ });
102
+ it('follows a symlink to a plugin checkout elsewhere', async () => {
103
+ const checkout = join(tempDir, 'checkout');
104
+ writePlugin(tempDir, 'checkout', { 'index.js': pluginSource(['linked']) });
105
+ symlinkSync(checkout, join(pluginsDirectory, 'linked'));
106
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
107
+ expect(plugins.map((plugin) => plugin.name)).toEqual(['linked']);
108
+ expect(serviceNames(plugins[0])).toEqual(['linked']);
109
+ });
110
+ it('supports an asynchronous factory', async () => {
111
+ writePlugin(pluginsDirectory, 'foo', {
112
+ 'index.js': pluginSource(['foo'], { asyncFactory: true }),
113
+ });
114
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
115
+ expect(serviceNames(plugins[0])).toEqual(['foo']);
116
+ });
117
+ it('loads a plugin without credentials types as contributing none', async () => {
118
+ writePlugin(pluginsDirectory, 'foo', { 'index.js': pluginSource(['foo']) });
119
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
120
+ expect(plugins[0].apiCredentialsTypes).toEqual([]);
121
+ });
122
+ it('loads the credentials classes a plugin defines', async () => {
123
+ writePlugin(pluginsDirectory, 'foo', {
124
+ 'index.js': pluginSource(['foo'], {
125
+ apiCredentialsTypes: `[
126
+ class FooToken extends sdk.AuthorizationBearer {
127
+ static objectType = 'fooToken';
128
+ static fromJSON(data) {
129
+ return new FooToken(data.token);
130
+ }
131
+ },
132
+ ]`,
133
+ }),
134
+ });
135
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
136
+ const [type] = plugins[0].apiCredentialsTypes;
137
+ expect(type.objectType).toBe('fooToken');
138
+ expect(type.fromJSON({ objectType: 'fooToken', token: 't' })).toBeInstanceOf(AuthorizationBearer);
139
+ });
140
+ describe('entry file', () => {
141
+ it('honors the main field of package.json', async () => {
142
+ writePlugin(pluginsDirectory, 'foo', {
143
+ 'package.json': JSON.stringify({ type: 'module', main: 'dist/plugin.js' }),
144
+ 'dist/plugin.js': pluginSource(['foo']),
145
+ 'index.js': 'throw new Error("index.js must not be loaded");',
146
+ });
147
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
148
+ expect(serviceNames(plugins[0])).toEqual(['foo']);
149
+ });
150
+ it.each([
151
+ ['a string', './dist/plugin.js'],
152
+ ['a root subpath', { '.': './dist/plugin.js' }],
153
+ ['conditions', { import: './dist/plugin.js', require: './dist/plugin.cjs' }],
154
+ [
155
+ 'a root subpath with conditions',
156
+ { '.': { types: './x.d.ts', default: './dist/plugin.js' } },
157
+ ],
158
+ ])('honors an exports field of package.json that is %s', async (_description, exportsField) => {
159
+ writePlugin(pluginsDirectory, 'foo', {
160
+ 'package.json': JSON.stringify({ type: 'module', main: 'wrong.js', exports: exportsField }),
161
+ 'dist/plugin.js': pluginSource(['foo']),
162
+ });
163
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
164
+ expect(serviceNames(plugins[0])).toEqual(['foo']);
165
+ });
166
+ it('falls back from an exports field without a usable entry to main', async () => {
167
+ writePlugin(pluginsDirectory, 'foo', {
168
+ 'package.json': JSON.stringify({
169
+ type: 'module',
170
+ main: 'dist/plugin.js',
171
+ exports: { './helpers': './dist/helpers.js' },
172
+ }),
173
+ 'dist/plugin.js': pluginSource(['foo']),
174
+ });
175
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
176
+ expect(serviceNames(plugins[0])).toEqual(['foo']);
177
+ });
178
+ it('rejects a plugin whose package.json is not valid JSON', async () => {
179
+ writePlugin(pluginsDirectory, 'foo', {
180
+ 'package.json': '{ not json',
181
+ 'index.js': pluginSource(['foo']),
182
+ });
183
+ await expectLoadError(pluginsDirectory, "Failed to load plugin 'foo': invalid package.json");
184
+ });
185
+ it('rejects a plugin whose entry file is missing, hinting at an unbuilt checkout', async () => {
186
+ writePlugin(pluginsDirectory, 'foo', {
187
+ 'package.json': JSON.stringify({ type: 'module', main: 'dist/index.js' }),
188
+ 'src/index.ts': 'export default () => ({ latchkeyVersion: "^3.15.0", services: [] });',
189
+ });
190
+ await expectLoadError(pluginsDirectory, 'has to be built first');
191
+ });
192
+ });
193
+ describe('validation', () => {
194
+ it('rejects a plugin that fails to import', async () => {
195
+ writePlugin(pluginsDirectory, 'foo', { 'index.js': 'export default (' });
196
+ await expectLoadError(pluginsDirectory, "Failed to load plugin 'foo': could not import");
197
+ });
198
+ it('rejects a plugin without a default export', async () => {
199
+ writePlugin(pluginsDirectory, 'foo', { 'index.js': 'export const services = [];' });
200
+ await expectLoadError(pluginsDirectory, 'must have a default export that is a function');
201
+ });
202
+ it('rejects a plugin whose default export is not a function', async () => {
203
+ writePlugin(pluginsDirectory, 'foo', {
204
+ 'index.js': 'export default { latchkeyVersion: "^3.15.0", services: [] };',
205
+ });
206
+ await expectLoadError(pluginsDirectory, 'must have a default export that is a function');
207
+ });
208
+ it('rejects a plugin whose factory throws', async () => {
209
+ writePlugin(pluginsDirectory, 'foo', {
210
+ 'index.js': 'export default () => { throw new Error("no network"); };',
211
+ });
212
+ await expectLoadError(pluginsDirectory, 'the plugin factory threw: no network');
213
+ });
214
+ it('rejects a plugin whose factory does not return an object', async () => {
215
+ writePlugin(pluginsDirectory, 'foo', { 'index.js': 'export default () => undefined;' });
216
+ await expectLoadError(pluginsDirectory, 'did not return an object');
217
+ });
218
+ it('rejects a plugin that does not declare the Latchkey versions it supports', async () => {
219
+ writePlugin(pluginsDirectory, 'foo', {
220
+ 'index.js': 'export default () => ({ services: [] });',
221
+ });
222
+ await expectLoadError(pluginsDirectory, "'latchkeyVersion' must be a string");
223
+ });
224
+ it('rejects a plugin declaring the Latchkey version as a number', async () => {
225
+ writePlugin(pluginsDirectory, 'foo', {
226
+ 'index.js': pluginSource(['foo'], { latchkeyVersion: 3 }),
227
+ });
228
+ await expectLoadError(pluginsDirectory, "'latchkeyVersion' must be a string");
229
+ });
230
+ it('rejects a plugin declaring an invalid version range', async () => {
231
+ writePlugin(pluginsDirectory, 'foo', {
232
+ 'index.js': pluginSource(['foo'], { latchkeyVersion: 'latest and greatest' }),
233
+ });
234
+ await expectLoadError(pluginsDirectory, "'latest and greatest' is not a valid version range for 'latchkeyVersion'");
235
+ });
236
+ it.each(['^4.0.0', '^3.16.0', '~3.14.0', '<3.15.2', '2.x'])('rejects a plugin whose version range %s excludes this Latchkey', async (latchkeyVersion) => {
237
+ writePlugin(pluginsDirectory, 'foo', {
238
+ 'index.js': pluginSource(['foo'], { latchkeyVersion }),
239
+ });
240
+ await expectLoadError(pluginsDirectory, `it supports Latchkey ${latchkeyVersion}, but this is Latchkey ${SDK_VERSION}.`);
241
+ });
242
+ it.each(['^3.15.0', '^3.0.0', '~3.15.0', '3.15.2', '3.x', '>=3.15.0 <4', '*'])('accepts a plugin whose version range %s includes this Latchkey', async (latchkeyVersion) => {
243
+ writePlugin(pluginsDirectory, 'foo', {
244
+ 'index.js': pluginSource(['foo'], { latchkeyVersion }),
245
+ });
246
+ const plugins = await loadPlugins(pluginsDirectory, SDK);
247
+ expect(serviceNames(plugins[0])).toEqual(['foo']);
248
+ });
249
+ it('rejects a plugin without a services array', async () => {
250
+ writePlugin(pluginsDirectory, 'foo', {
251
+ 'index.js': 'export default () => ({ latchkeyVersion: "^3.15.0", services: {} });',
252
+ });
253
+ await expectLoadError(pluginsDirectory, "'services' must be an array");
254
+ });
255
+ it('rejects a service that does not extend the Service class', async () => {
256
+ writePlugin(pluginsDirectory, 'foo', {
257
+ 'index.js': `export default () => ({
258
+ latchkeyVersion: '^3.15.0',
259
+ services: [{ name: 'foo', displayName: 'Foo', baseApiUrls: [], loginUrl: '', info: '' }],
260
+ });`,
261
+ });
262
+ await expectLoadError(pluginsDirectory, 'must extend the Service class');
263
+ });
264
+ it('rejects a service extending a Service class other than the one in the sdk', async () => {
265
+ // What a second, separately installed copy of latchkey would produce.
266
+ writePlugin(pluginsDirectory, 'foo', {
267
+ 'index.js': `
268
+ class Service {}
269
+ class Foo extends Service { name = 'foo'; }
270
+ export default () => ({ latchkeyVersion: '^3.15.0', services: [new Foo()] });
271
+ `,
272
+ });
273
+ await expectLoadError(pluginsDirectory, 'separately installed latchkey');
274
+ });
275
+ it('rejects credentials types that are not an array', async () => {
276
+ writePlugin(pluginsDirectory, 'foo', {
277
+ 'index.js': pluginSource(['foo'], { apiCredentialsTypes: '{}' }),
278
+ });
279
+ await expectLoadError(pluginsDirectory, "'apiCredentialsTypes' must be an array");
280
+ });
281
+ it('rejects a credentials class without a static fromJSON', async () => {
282
+ writePlugin(pluginsDirectory, 'foo', {
283
+ 'index.js': pluginSource(['foo'], {
284
+ apiCredentialsTypes: "[class FooToken { static objectType = 'fooToken'; }]",
285
+ }),
286
+ });
287
+ await expectLoadError(pluginsDirectory, "every entry of 'apiCredentialsTypes' must be a credentials class with a static " +
288
+ "'objectType' and a static 'fromJSON'.");
289
+ });
290
+ it('reports the first broken plugin and loads none', async () => {
291
+ writePlugin(pluginsDirectory, 'a-fine', { 'index.js': pluginSource(['fine']) });
292
+ writePlugin(pluginsDirectory, 'b-broken', { 'index.js': 'export default 42;' });
293
+ await expectLoadError(pluginsDirectory, "Failed to load plugin 'b-broken'");
294
+ });
295
+ });
296
+ });
297
+ describe('isLatchkeyVersionSupported', () => {
298
+ it('accepts a release within the range', () => {
299
+ expect(isLatchkeyVersionSupported('^3.15.0', '3.15.0')).toBe(true);
300
+ expect(isLatchkeyVersionSupported('^3.15.0', '3.99.1')).toBe(true);
301
+ });
302
+ it('rejects a release outside the range', () => {
303
+ expect(isLatchkeyVersionSupported('^3.15.0', '3.14.9')).toBe(false);
304
+ expect(isLatchkeyVersionSupported('^3.15.0', '4.0.0')).toBe(false);
305
+ });
306
+ it('accepts a prerelease of a version within the range', () => {
307
+ expect(isLatchkeyVersionSupported('^3.15.0', '3.16.0-dev.1')).toBe(true);
308
+ });
309
+ it('rejects a prerelease of a version outside the range', () => {
310
+ expect(isLatchkeyVersionSupported('^3.15.0', '3.15.0-dev.1')).toBe(false);
311
+ expect(isLatchkeyVersionSupported('^3.15.0', '4.0.0-rc.1')).toBe(false);
312
+ });
313
+ });
314
+ describe('combineWithPluginServices', () => {
315
+ function loadedPlugin(name, services, apiCredentialsTypes = []) {
316
+ return { name, directory: `/plugins/${name}`, services, apiCredentialsTypes };
317
+ }
318
+ class NamedService extends Service {
319
+ name;
320
+ displayName;
321
+ baseApiUrls = [];
322
+ loginUrl = '';
323
+ info = '';
324
+ credentialCheckCurlArguments = [];
325
+ constructor(name) {
326
+ super();
327
+ this.name = name;
328
+ this.displayName = name;
329
+ }
330
+ setCredentialsExample(serviceName) {
331
+ return `latchkey auth set ${serviceName}`;
332
+ }
333
+ }
334
+ it('appends plugin services after the built-in ones, in plugin order', () => {
335
+ const foo = new NamedService('foo');
336
+ const bar = new NamedService('bar');
337
+ const combined = combineWithPluginServices([SLACK], [loadedPlugin('a', [foo]), loadedPlugin('b', [bar])]);
338
+ expect(combined).toEqual([SLACK, foo, bar]);
339
+ });
340
+ it('refuses a plugin service named like a built-in one', () => {
341
+ expect(() => combineWithPluginServices([SLACK], [loadedPlugin('a', [new NamedService('slack')])])).toThrow("Failed to load plugin 'a': service 'slack' is already provided by Latchkey itself.");
342
+ });
343
+ it('refuses a plugin service named like one from an earlier plugin', () => {
344
+ expect(() => combineWithPluginServices([], [loadedPlugin('a', [new NamedService('foo')]), loadedPlugin('b', [new NamedService('foo')])])).toThrow("Failed to load plugin 'b': service 'foo' is already provided by plugin 'a'.");
345
+ });
346
+ });
347
+ describe('combineWithPluginApiCredentialsTypes', () => {
348
+ function loadedPlugin(name, apiCredentialsTypes) {
349
+ return { name, directory: `/plugins/${name}`, services: [], apiCredentialsTypes };
350
+ }
351
+ function namedType(objectType) {
352
+ return { objectType, fromJSON: () => new AuthorizationBearer('') };
353
+ }
354
+ it('appends plugin credentials types after the built-in ones, in plugin order', () => {
355
+ const foo = namedType('foo');
356
+ const bar = namedType('bar');
357
+ const combined = combineWithPluginApiCredentialsTypes(BUILTIN_API_CREDENTIALS_TYPES, [
358
+ loadedPlugin('a', [foo]),
359
+ loadedPlugin('b', [bar]),
360
+ ]);
361
+ expect(combined).toEqual([...BUILTIN_API_CREDENTIALS_TYPES, foo, bar]);
362
+ });
363
+ it('refuses a plugin credentials type named like a built-in one', () => {
364
+ expect(() => combineWithPluginApiCredentialsTypes(BUILTIN_API_CREDENTIALS_TYPES, [
365
+ loadedPlugin('a', [namedType('oauth')]),
366
+ ])).toThrow("Failed to load plugin 'a': credentials type 'oauth' is already provided by Latchkey itself.");
367
+ });
368
+ it('refuses a plugin credentials type named like one from an earlier plugin', () => {
369
+ expect(() => combineWithPluginApiCredentialsTypes([], [loadedPlugin('a', [namedType('foo')]), loadedPlugin('b', [namedType('foo')])])).toThrow("Failed to load plugin 'b': credentials type 'foo' is already provided by plugin 'a'.");
370
+ });
371
+ });
372
+ // ─── End to end: a plugin served by the real CLI ──────────────────────────────
373
+ const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
374
+ const projectRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
375
+ const devShimPath = join(projectRoot, 'scripts', 'latchkey');
376
+ /**
377
+ * What a real plugin looks like: an ES module package with nothing installed
378
+ * in its own node_modules, taking everything, zod included, from the sdk. It
379
+ * also brings a credentials class of its own, which the sdk lets it register.
380
+ */
381
+ const REAL_PLUGIN_SOURCE = `
382
+ export default (sdk) => {
383
+ const { Service, AuthorizationBearer, buildPreparedCredentials, z } = sdk;
384
+
385
+ const ExampleTokenSchema = z.object({ objectType: z.literal('exampleToken'), token: z.string() });
386
+
387
+ class ExampleToken {
388
+ static objectType = 'exampleToken';
389
+ objectType = ExampleToken.objectType;
390
+ constructor(token) {
391
+ this.token = token;
392
+ }
393
+ static fromJSON(data) {
394
+ return new ExampleToken(ExampleTokenSchema.parse(data).token);
395
+ }
396
+ injectIntoCurlCall(curlArguments) {
397
+ return Promise.resolve(['-H', \`X-Example-Token: \${this.token}\`, ...curlArguments]);
398
+ }
399
+ isExpired() {
400
+ return undefined;
401
+ }
402
+ toJSON() {
403
+ return { objectType: this.objectType, token: this.token };
404
+ }
405
+ }
406
+
407
+ class Example extends Service {
408
+ name = 'example';
409
+ displayName = 'Example';
410
+ baseApiUrls = ['https://api.example.com/'];
411
+ loginUrl = 'https://example.com/login';
412
+ info = 'Example plugin service.';
413
+ credentialCheckCurlArguments = ['https://api.example.com/me'];
414
+
415
+ setCredentialsExample(serviceName) {
416
+ return \`latchkey auth set \${serviceName} -H "Authorization: Bearer <token>"\`;
417
+ }
418
+
419
+ prepareFromJson(parsedJson) {
420
+ return buildPreparedCredentials(
421
+ this.name,
422
+ z.object({ token: z.string() }),
423
+ parsedJson,
424
+ (input) => new AuthorizationBearer(input.token)
425
+ );
426
+ }
427
+
428
+ getCredentialsNoCurl(noCurlArguments) {
429
+ return new ExampleToken(noCurlArguments[0]);
430
+ }
431
+ }
432
+
433
+ return {
434
+ latchkeyVersion: ${JSON.stringify(`^${VERSION}`)},
435
+ services: [new Example()],
436
+ apiCredentialsTypes: [ExampleToken],
437
+ };
438
+ };
439
+ `;
440
+ function runCli(latchkeyDirectory, args) {
441
+ try {
442
+ const stdout = execFileSync(devShimPath, args, {
443
+ cwd: projectRoot,
444
+ encoding: 'utf-8',
445
+ env: {
446
+ ...process.env,
447
+ LATCHKEY_DIRECTORY: latchkeyDirectory,
448
+ LATCHKEY_ENCRYPTION_KEY: TEST_ENCRYPTION_KEY,
449
+ LATCHKEY_DISABLE_COUNTING: '1',
450
+ },
451
+ stdio: ['pipe', 'pipe', 'pipe'],
452
+ });
453
+ return { exitCode: 0, stdout, stderr: '' };
454
+ }
455
+ catch (error) {
456
+ const failure = error;
457
+ return { exitCode: failure.status ?? 1, stdout: failure.stdout, stderr: failure.stderr };
458
+ }
459
+ }
460
+ describe('plugins loaded by the CLI', () => {
461
+ let latchkeyDirectory;
462
+ beforeEach(() => {
463
+ latchkeyDirectory = mkdtempSync(join(tmpdir(), 'latchkey-plugins-cli-test-'));
464
+ writePlugin(join(latchkeyDirectory, 'plugins'), 'latchkey-plugin-example', {
465
+ 'package.json': JSON.stringify({ name: 'latchkey-plugin-example', type: 'module' }),
466
+ 'index.js': REAL_PLUGIN_SOURCE,
467
+ });
468
+ });
469
+ afterEach(() => {
470
+ rmSync(latchkeyDirectory, { recursive: true, force: true });
471
+ });
472
+ it('serves a plugin service built from the sdk, zod included, like a built-in one', () => {
473
+ const preparation = runCli(latchkeyDirectory, ['auth', 'prepare', 'example', '{"token":"t"}']);
474
+ expect(preparation.stderr).toBe('');
475
+ expect(preparation.exitCode).toBe(0);
476
+ const info = runCli(latchkeyDirectory, ['services', 'info', 'example', '--offline']);
477
+ expect(info.stderr).toBe('');
478
+ expect(info.exitCode).toBe(0);
479
+ const parsedInfo = JSON.parse(info.stdout);
480
+ expect(parsedInfo.type).toBe('built-in');
481
+ expect(parsedInfo.developerNotes).toBe('Example plugin service.');
482
+ }, 60_000);
483
+ it('stores and reads back credentials of a class the plugin defines', () => {
484
+ const setting = runCli(latchkeyDirectory, ['auth', 'set-nocurl', 'example', 't0ken']);
485
+ expect(setting.stderr).toBe('');
486
+ expect(setting.exitCode).toBe(0);
487
+ const info = runCli(latchkeyDirectory, ['services', 'info', 'example', '--offline']);
488
+ expect(info.stderr).toBe('');
489
+ expect(info.exitCode).toBe(0);
490
+ const parsedInfo = JSON.parse(info.stdout);
491
+ expect(Object.values(parsedInfo.credentials)).toEqual([
492
+ { credentialStatus: 'unknown', credentialType: 'exampleToken' },
493
+ ]);
494
+ }, 60_000);
495
+ it('refuses to start with a broken plugin and names it', () => {
496
+ writePlugin(join(latchkeyDirectory, 'plugins'), 'broken', {
497
+ 'package.json': JSON.stringify({ type: 'module' }),
498
+ 'index.js': 'export default () => ({ latchkeyVersion: "^1.0.0", services: [] });',
499
+ });
500
+ const result = runCli(latchkeyDirectory, ['services', 'list', '--builtin']);
501
+ expect(result.exitCode).toBe(1);
502
+ expect(result.stderr).toContain("Error: Failed to load plugin 'broken'");
503
+ expect(result.stderr).toContain(`it supports Latchkey ^1.0.0, but this is Latchkey ${VERSION}.`);
504
+ }, 60_000);
505
+ });
506
+ //# sourceMappingURL=plugins.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins.test.js","sourceRoot":"","sources":["../../tests/plugins.test.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ import { PrepareInputInvalidError, PrepareNotSupportedError, } from '../src/serv
10
10
  import { MockService } from './mockService.js';
11
11
  import { GOOGLE_GMAIL } from '../src/services/google/gmail.js';
12
12
  import { NOTION_MCP } from '../src/services/notion-mcp.js';
13
+ import { FASTMAIL } from '../src/services/fastmail.js';
13
14
  import { RegisteredService } from '../src/services/core/registered.js';
14
15
  import { ServiceRegistry } from '../src/serviceRegistry.js';
15
16
  import { saveBrowserConfig } from '../src/configDataStore.js';
@@ -755,12 +756,72 @@ describe('operations', () => {
755
756
  expect(() => prepareService(registry, store, 'notion-mcp', JSON.stringify({ clientId: 'a', clientSecret: 'b' }))).toThrow(PrepareInputInvalidError);
756
757
  expect(store.getPreparation('notion-mcp')).toBeNull();
757
758
  });
758
- it('rejects notion-mcp input missing clientId', () => {
759
+ it('stores a prepared redirect URI alongside the client for a Google service', () => {
760
+ const registry = new ServiceRegistry([GOOGLE_GMAIL]);
761
+ const store = createApiCredentialStore();
762
+ prepareService(registry, store, 'google-gmail', JSON.stringify({
763
+ clientId: 'cid',
764
+ clientSecret: 'csecret',
765
+ redirectUri: 'https://example.com/oauth-callback/',
766
+ }));
767
+ expect(store.getPreparation('google-gmail').redirectUri).toBe('https://example.com/oauth-callback/');
768
+ });
769
+ it('stores a prepared redirect URI for notion-mcp', () => {
770
+ const registry = new ServiceRegistry([NOTION_MCP]);
771
+ const store = createApiCredentialStore();
772
+ prepareService(registry, store, 'notion-mcp', JSON.stringify({
773
+ clientId: 'notion-client-id',
774
+ redirectUri: 'https://example.com/oauth-callback/',
775
+ }));
776
+ expect(store.getPreparation('notion-mcp').redirectUri).toBe('https://example.com/oauth-callback/');
777
+ });
778
+ it('leaves the redirect URI unset when the preparation omits it', () => {
779
+ const registry = new ServiceRegistry([NOTION_MCP]);
780
+ const store = createApiCredentialStore();
781
+ prepareService(registry, store, 'notion-mcp', JSON.stringify({ clientId: 'notion-client' }));
782
+ expect(store.getPreparation('notion-mcp').redirectUri).toBeUndefined();
783
+ });
784
+ it('rejects a redirect URI that is not a URL, storing nothing', () => {
785
+ const registry = new ServiceRegistry([NOTION_MCP]);
786
+ const store = createApiCredentialStore();
787
+ expect(() => prepareService(registry, store, 'notion-mcp', JSON.stringify({ clientId: 'notion-client', redirectUri: 'example.com/callback' }))).toThrow(PrepareInputInvalidError);
788
+ expect(store.getPreparation('notion-mcp')).toBeNull();
789
+ });
790
+ it('rejects a redirect URI that is not http(s)', () => {
759
791
  const registry = new ServiceRegistry([NOTION_MCP]);
760
792
  const store = createApiCredentialStore();
761
- expect(() => prepareService(registry, store, 'notion-mcp', '{}')).toThrow(PrepareInputInvalidError);
793
+ expect(() => prepareService(registry, store, 'notion-mcp', JSON.stringify({ clientId: 'notion-client', redirectUri: 'mailto:someone@example.com' }))).toThrow(PrepareInputInvalidError);
762
794
  expect(store.getPreparation('notion-mcp')).toBeNull();
763
795
  });
796
+ it('rejects notion-mcp input with neither clientId nor redirectUri', () => {
797
+ const registry = new ServiceRegistry([NOTION_MCP]);
798
+ const store = createApiCredentialStore();
799
+ expect(() => prepareService(registry, store, 'notion-mcp', '{}')).toThrow(/at least one of clientId or redirectUri/);
800
+ expect(store.getPreparation('notion-mcp')).toBeNull();
801
+ });
802
+ it('rejects an empty notion-mcp clientId', () => {
803
+ const registry = new ServiceRegistry([NOTION_MCP]);
804
+ const store = createApiCredentialStore();
805
+ expect(() => prepareService(registry, store, 'notion-mcp', JSON.stringify({ clientId: '' }))).toThrow(PrepareInputInvalidError);
806
+ expect(store.getPreparation('notion-mcp')).toBeNull();
807
+ });
808
+ // Given only a redirect URI, login still registers a client dynamically —
809
+ // an empty client id is how the preparation says so — but with that
810
+ // redirect URI instead of the loopback one.
811
+ it.each([
812
+ ['notion-mcp', NOTION_MCP],
813
+ ['fastmail', FASTMAIL],
814
+ ])('stores a redirect URI without a client id for %s', (serviceName, service) => {
815
+ const registry = new ServiceRegistry([service]);
816
+ const store = createApiCredentialStore();
817
+ prepareService(registry, store, serviceName, JSON.stringify({ redirectUri: 'https://example.com/oauth-callback/' }));
818
+ const preparation = store.getPreparation(serviceName);
819
+ expect(preparation).toBeInstanceOf(OAuthCredentials);
820
+ expect(preparation.clientId).toBe('');
821
+ expect(preparation.clientSecret).toBe('');
822
+ expect(preparation.redirectUri).toBe('https://example.com/oauth-callback/');
823
+ expect(preparation.accessToken).toBeUndefined();
824
+ });
764
825
  });
765
826
  });
766
827
  //# sourceMappingURL=sharedOperations.test.js.map