latchkey 0.2.0 → 1.0.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.
- package/README.md +98 -57
- package/dist/integrations/SKILL.md +25 -19
- package/dist/package.json +2 -2
- package/dist/scripts/codegen/codeGenerator.d.ts +27 -0
- package/dist/scripts/codegen/codeGenerator.d.ts.map +1 -0
- package/dist/scripts/codegen/codeGenerator.js +220 -0
- package/dist/scripts/codegen/codeGenerator.js.map +1 -0
- package/dist/scripts/codegen/index.d.ts +27 -0
- package/dist/scripts/codegen/index.d.ts.map +1 -0
- package/dist/scripts/codegen/index.js +189 -0
- package/dist/scripts/codegen/index.js.map +1 -0
- package/dist/scripts/codegen/injectedScript.d.ts +6 -0
- package/dist/scripts/codegen/injectedScript.d.ts.map +1 -0
- package/dist/scripts/codegen/injectedScript.js +657 -0
- package/dist/scripts/codegen/injectedScript.js.map +1 -0
- package/dist/scripts/codegen/requestMetadataCollector.d.ts +15 -0
- package/dist/scripts/codegen/requestMetadataCollector.d.ts.map +1 -0
- package/dist/scripts/codegen/requestMetadataCollector.js +48 -0
- package/dist/scripts/codegen/requestMetadataCollector.js.map +1 -0
- package/dist/scripts/codegen/types.d.ts +77 -0
- package/dist/scripts/codegen/types.d.ts.map +1 -0
- package/dist/scripts/codegen/types.js +10 -0
- package/dist/scripts/codegen/types.js.map +1 -0
- package/dist/scripts/codegen.d.ts +24 -0
- package/dist/scripts/codegen.d.ts.map +1 -0
- package/dist/scripts/codegen.js +95 -0
- package/dist/scripts/codegen.js.map +1 -0
- package/dist/scripts/cryptFile.js +7 -2
- package/dist/scripts/cryptFile.js.map +1 -1
- package/dist/src/apiCredentialStore.d.ts +1 -0
- package/dist/src/apiCredentialStore.d.ts.map +1 -1
- package/dist/src/apiCredentialStore.js +12 -0
- package/dist/src/apiCredentialStore.js.map +1 -1
- package/dist/src/apiCredentials.d.ts +33 -0
- package/dist/src/apiCredentials.d.ts.map +1 -1
- package/dist/src/apiCredentials.js +36 -0
- package/dist/src/apiCredentials.js.map +1 -1
- package/dist/src/cli.js +3 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +158 -126
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +13 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +56 -16
- package/dist/src/config.js.map +1 -1
- package/dist/src/encryptedStorage.d.ts +1 -2
- package/dist/src/encryptedStorage.d.ts.map +1 -1
- package/dist/src/encryptedStorage.js +18 -38
- package/dist/src/encryptedStorage.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/keychain.d.ts +0 -4
- package/dist/src/keychain.d.ts.map +1 -1
- package/dist/src/keychain.js +0 -13
- package/dist/src/keychain.js.map +1 -1
- package/dist/src/oauthUtils.d.ts +1 -1
- package/dist/src/oauthUtils.d.ts.map +1 -1
- package/dist/src/playwrightUtils.d.ts +6 -0
- package/dist/src/playwrightUtils.d.ts.map +1 -1
- package/dist/src/playwrightUtils.js +12 -0
- package/dist/src/playwrightUtils.js.map +1 -1
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +20 -4
- package/dist/src/registry.js.map +1 -1
- package/dist/src/services/base.d.ts +20 -18
- package/dist/src/services/base.d.ts.map +1 -1
- package/dist/src/services/base.js +37 -1
- package/dist/src/services/base.js.map +1 -1
- package/dist/src/services/discord.d.ts +2 -3
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +3 -22
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +2 -3
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +3 -22
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/github.d.ts +2 -3
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +3 -22
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/google.d.ts +3 -4
- package/dist/src/services/google.d.ts.map +1 -1
- package/dist/src/services/google.js +21 -43
- package/dist/src/services/google.js.map +1 -1
- package/dist/src/services/index.d.ts +2 -2
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +2 -1
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/linear.d.ts +2 -3
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +3 -23
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/mailchimp.d.ts +11 -0
- package/dist/src/services/mailchimp.d.ts.map +1 -0
- package/dist/src/services/mailchimp.js +16 -0
- package/dist/src/services/mailchimp.js.map +1 -0
- package/dist/src/services/notion.d.ts +2 -3
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +3 -22
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/services/slack.d.ts +1 -1
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +2 -5
- package/dist/src/services/slack.js.map +1 -1
- package/dist/tests/apiCredentials.test.js +59 -1
- package/dist/tests/apiCredentials.test.js.map +1 -1
- package/dist/tests/cli.test.js +270 -128
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/playwrightDownload.test.js +2 -2
- package/dist/tests/playwrightDownload.test.js.map +1 -1
- package/dist/tests/registry.test.js +14 -2
- package/dist/tests/registry.test.js.map +1 -1
- package/dist/tests/servicesAgainstRecordings.test.js +3 -0
- package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
- package/package.json +2 -2
package/dist/tests/cli.test.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, existsSync
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
import { extractUrlFromCurlArguments, registerCommands, } from '../src/cliCommands.js';
|
|
8
|
+
import { BrowserFlowsNotSupportedError } from '../src/playwrightUtils.js';
|
|
8
9
|
import { EncryptedStorage } from '../src/encryptedStorage.js';
|
|
9
10
|
import { Config } from '../src/config.js';
|
|
10
11
|
import { Registry } from '../src/registry.js';
|
|
@@ -122,7 +123,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
122
123
|
encryptionKeyOverride: overrides.encryptionKeyOverride ?? TEST_ENCRYPTION_KEY,
|
|
123
124
|
serviceName: overrides.serviceName ?? defaultConfig.serviceName,
|
|
124
125
|
accountName: overrides.accountName ?? defaultConfig.accountName,
|
|
126
|
+
browserDisabled: overrides.browserDisabled ?? false,
|
|
125
127
|
checkSensitiveFilePermissions: () => undefined,
|
|
128
|
+
checkSystemPrerequisites: () => undefined,
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
function createMockDependencies(overrides = {}) {
|
|
@@ -184,67 +187,60 @@ describe('CLI commands with dependency injection', () => {
|
|
|
184
187
|
afterEach(() => {
|
|
185
188
|
rmSync(tempDir, { recursive: true, force: true });
|
|
186
189
|
});
|
|
187
|
-
describe('services command', () => {
|
|
188
|
-
it('should list all services as
|
|
190
|
+
describe('services list command', () => {
|
|
191
|
+
it('should list all services as JSON', async () => {
|
|
189
192
|
const deps = createMockDependencies();
|
|
190
|
-
await runCommand(['services'], deps);
|
|
193
|
+
await runCommand(['services', 'list'], deps);
|
|
191
194
|
expect(logs).toHaveLength(1);
|
|
192
|
-
const services = (logs[0] ?? '')
|
|
195
|
+
const services = JSON.parse(logs[0] ?? '');
|
|
193
196
|
expect(services).toContain('slack');
|
|
194
197
|
});
|
|
195
198
|
});
|
|
196
|
-
describe('info command', () => {
|
|
197
|
-
it('should show
|
|
198
|
-
const deps = createMockDependencies();
|
|
199
|
-
await runCommand(['info', 'slack'], deps);
|
|
200
|
-
expect(logs).toHaveLength(1);
|
|
201
|
-
expect(logs[0]).toBe('Test info for Slack service.');
|
|
202
|
-
});
|
|
203
|
-
it('should return error for unknown service', async () => {
|
|
204
|
-
const deps = createMockDependencies();
|
|
205
|
-
await runCommand(['info', 'unknown-service'], deps);
|
|
206
|
-
expect(exitCode).toBe(1);
|
|
207
|
-
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
describe('status command', () => {
|
|
211
|
-
it('should return missing when no credentials are stored', async () => {
|
|
199
|
+
describe('services info command', () => {
|
|
200
|
+
it('should show login options, credentials status, and developer notes', async () => {
|
|
212
201
|
const storePath = join(tempDir, 'credentials.json');
|
|
213
202
|
writeSecureFile(storePath, '{}');
|
|
214
203
|
const deps = createMockDependencies({
|
|
215
204
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
216
205
|
});
|
|
217
|
-
await runCommand(['
|
|
218
|
-
expect(logs).
|
|
206
|
+
await runCommand(['services', 'info', 'slack'], deps);
|
|
207
|
+
expect(logs).toHaveLength(1);
|
|
208
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
209
|
+
expect(info.authOptions).toEqual(['browser', 'set']);
|
|
210
|
+
expect(info.credentialStatus).toBe('missing');
|
|
211
|
+
expect(info.developerNotes).toBe('Test info for Slack service.');
|
|
219
212
|
});
|
|
220
|
-
it('should
|
|
213
|
+
it('should show auth set only for services without browser login', async () => {
|
|
221
214
|
const storePath = join(tempDir, 'credentials.json');
|
|
222
|
-
writeSecureFile(storePath,
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
writeSecureFile(storePath, '{}');
|
|
216
|
+
const noLoginService = {
|
|
217
|
+
name: 'nologin',
|
|
218
|
+
displayName: 'No Login Service',
|
|
219
|
+
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
220
|
+
loginUrl: 'https://nologin.example.com',
|
|
221
|
+
info: 'A service without browser login support.',
|
|
222
|
+
credentialCheckCurlArguments: [],
|
|
223
|
+
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Missing),
|
|
224
|
+
};
|
|
225
225
|
const deps = createMockDependencies({
|
|
226
|
+
registry: new Registry([noLoginService]),
|
|
226
227
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
227
228
|
});
|
|
228
|
-
await runCommand(['
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
it('should return error for unknown service', async () => {
|
|
232
|
-
const deps = createMockDependencies();
|
|
233
|
-
await runCommand(['status', 'unknown-service'], deps);
|
|
234
|
-
expect(exitCode).toBe(1);
|
|
235
|
-
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
229
|
+
await runCommand(['services', 'info', 'nologin'], deps);
|
|
230
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
231
|
+
expect(info.authOptions).toEqual(['set']);
|
|
236
232
|
});
|
|
237
|
-
it('should
|
|
233
|
+
it('should not list browser in authOptions when LATCHKEY_DISABLE_BROWSER is in effect', async () => {
|
|
238
234
|
const storePath = join(tempDir, 'credentials.json');
|
|
239
235
|
writeSecureFile(storePath, '{}');
|
|
240
236
|
const deps = createMockDependencies({
|
|
241
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
237
|
+
config: createMockConfig({ credentialStorePath: storePath, browserDisabled: true }),
|
|
242
238
|
});
|
|
243
|
-
await runCommand(['
|
|
244
|
-
|
|
245
|
-
expect(
|
|
239
|
+
await runCommand(['services', 'info', 'slack'], deps);
|
|
240
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
241
|
+
expect(info.authOptions).toEqual(['set']);
|
|
246
242
|
});
|
|
247
|
-
it('should
|
|
243
|
+
it('should show valid credentials status when credentials are valid', async () => {
|
|
248
244
|
const storePath = join(tempDir, 'credentials.json');
|
|
249
245
|
writeSecureFile(storePath, JSON.stringify({
|
|
250
246
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
@@ -252,9 +248,15 @@ describe('CLI commands with dependency injection', () => {
|
|
|
252
248
|
const deps = createMockDependencies({
|
|
253
249
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
254
250
|
});
|
|
255
|
-
await runCommand(['
|
|
256
|
-
|
|
257
|
-
expect(
|
|
251
|
+
await runCommand(['services', 'info', 'slack'], deps);
|
|
252
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
253
|
+
expect(info.credentialStatus).toBe('valid');
|
|
254
|
+
});
|
|
255
|
+
it('should return error for unknown service', async () => {
|
|
256
|
+
const deps = createMockDependencies();
|
|
257
|
+
await runCommand(['services', 'info', 'unknown-service'], deps);
|
|
258
|
+
expect(exitCode).toBe(1);
|
|
259
|
+
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
258
260
|
});
|
|
259
261
|
});
|
|
260
262
|
describe('clear command', () => {
|
|
@@ -266,7 +268,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
266
268
|
const deps = createMockDependencies({
|
|
267
269
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
268
270
|
});
|
|
269
|
-
await runCommand(['clear', 'slack'], deps);
|
|
271
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
270
272
|
expect(logs.some((log) => log.includes('have been cleared'))).toBe(true);
|
|
271
273
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
272
274
|
expect(storedData.slack).toBeUndefined();
|
|
@@ -277,7 +279,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
277
279
|
const deps = createMockDependencies({
|
|
278
280
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
279
281
|
});
|
|
280
|
-
await runCommand(['clear', 'slack'], deps);
|
|
282
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
281
283
|
expect(logs.some((log) => log.includes('No API credentials found'))).toBe(true);
|
|
282
284
|
});
|
|
283
285
|
it('should return error for unknown service', async () => {
|
|
@@ -285,13 +287,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
285
287
|
const deps = createMockDependencies({
|
|
286
288
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
287
289
|
});
|
|
288
|
-
await runCommand(['clear', 'unknown-service'], deps);
|
|
290
|
+
await runCommand(['auth', 'clear', 'unknown-service'], deps);
|
|
289
291
|
expect(exitCode).toBe(1);
|
|
290
292
|
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
291
293
|
});
|
|
292
294
|
it('should use default config paths', async () => {
|
|
293
295
|
const deps = createMockDependencies();
|
|
294
|
-
await runCommand(['clear', 'slack'], deps);
|
|
296
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
295
297
|
// With default paths, should report no credentials found (not error about missing env var)
|
|
296
298
|
expect(logs.some((log) => log.includes('No API credentials found'))).toBe(true);
|
|
297
299
|
});
|
|
@@ -304,7 +306,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
304
306
|
const deps = createMockDependencies({
|
|
305
307
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
306
308
|
});
|
|
307
|
-
await runCommand(['clear', 'slack'], deps);
|
|
309
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
308
310
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
309
311
|
expect(storedData.slack).toBeUndefined();
|
|
310
312
|
expect(storedData.discord).toBeDefined();
|
|
@@ -318,7 +320,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
318
320
|
const deps = createMockDependencies({
|
|
319
321
|
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
320
322
|
});
|
|
321
|
-
await runCommand(['clear', '-y'], deps);
|
|
323
|
+
await runCommand(['auth', 'clear', '-y'], deps);
|
|
322
324
|
expect(existsSync(storePath)).toBe(false);
|
|
323
325
|
expect(existsSync(browserStatePath)).toBe(false);
|
|
324
326
|
expect(logs.some((log) => log.includes('Deleted credentials store'))).toBe(true);
|
|
@@ -330,10 +332,107 @@ describe('CLI commands with dependency injection', () => {
|
|
|
330
332
|
const deps = createMockDependencies({
|
|
331
333
|
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
332
334
|
});
|
|
333
|
-
await runCommand(['clear', '-y'], deps);
|
|
335
|
+
await runCommand(['auth', 'clear', '-y'], deps);
|
|
334
336
|
expect(logs.some((log) => log.includes('No files to delete'))).toBe(true);
|
|
335
337
|
});
|
|
336
338
|
});
|
|
339
|
+
describe('auth list command', () => {
|
|
340
|
+
it('should list stored credentials with their status', async () => {
|
|
341
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
342
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
343
|
+
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
344
|
+
}));
|
|
345
|
+
const deps = createMockDependencies({
|
|
346
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
347
|
+
});
|
|
348
|
+
await runCommand(['auth', 'list'], deps);
|
|
349
|
+
expect(logs).toHaveLength(1);
|
|
350
|
+
const entries = JSON.parse(logs[0] ?? '');
|
|
351
|
+
expect(entries.slack).toEqual({
|
|
352
|
+
credentialType: 'slack',
|
|
353
|
+
credentialStatus: 'valid',
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
it('should output empty object when no credentials are stored', async () => {
|
|
357
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
358
|
+
writeSecureFile(storePath, '{}');
|
|
359
|
+
const deps = createMockDependencies({
|
|
360
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
361
|
+
});
|
|
362
|
+
await runCommand(['auth', 'list'], deps);
|
|
363
|
+
expect(logs).toHaveLength(1);
|
|
364
|
+
const entries = JSON.parse(logs[0] ?? '');
|
|
365
|
+
expect(Object.keys(entries)).toHaveLength(0);
|
|
366
|
+
});
|
|
367
|
+
it('should treat unknown services as valid', async () => {
|
|
368
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
369
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
370
|
+
unknown: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: secret'] },
|
|
371
|
+
}));
|
|
372
|
+
const deps = createMockDependencies({
|
|
373
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
374
|
+
});
|
|
375
|
+
await runCommand(['auth', 'list'], deps);
|
|
376
|
+
expect(logs).toHaveLength(1);
|
|
377
|
+
const entries = JSON.parse(logs[0] ?? '');
|
|
378
|
+
expect(entries.unknown).toEqual({
|
|
379
|
+
credentialType: 'rawCurl',
|
|
380
|
+
credentialStatus: 'valid',
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
describe('auth set command', () => {
|
|
385
|
+
it('should store raw curl credentials', async () => {
|
|
386
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
387
|
+
writeSecureFile(storePath, '{}');
|
|
388
|
+
const deps = createMockDependencies({
|
|
389
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
390
|
+
});
|
|
391
|
+
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: secret', '-H', 'X-Other: value'], deps);
|
|
392
|
+
expect(logs).toContain('Credentials stored.');
|
|
393
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
394
|
+
expect(storedData.slack).toEqual({
|
|
395
|
+
objectType: 'rawCurl',
|
|
396
|
+
curlArguments: ['-H', 'X-Token: secret', '-H', 'X-Other: value'],
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
it('should return error for empty curl arguments', async () => {
|
|
400
|
+
const deps = createMockDependencies();
|
|
401
|
+
await runCommand(['auth', 'set', 'slack'], deps);
|
|
402
|
+
expect(exitCode).toBe(1);
|
|
403
|
+
expect(errorLogs.some((log) => log.includes("don't look like valid curl options"))).toBe(true);
|
|
404
|
+
expect(errorLogs.some((log) => log.includes('Authorization: Bearer'))).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
it('should return error when arguments lack curl switches', async () => {
|
|
407
|
+
const deps = createMockDependencies();
|
|
408
|
+
await runCommand(['auth', 'set', 'slack', 'my-raw-token-value'], deps);
|
|
409
|
+
expect(exitCode).toBe(1);
|
|
410
|
+
expect(errorLogs.some((log) => log.includes("don't look like valid curl options"))).toBe(true);
|
|
411
|
+
expect(errorLogs.some((log) => log.includes('Authorization: Bearer'))).toBe(true);
|
|
412
|
+
});
|
|
413
|
+
it('should return error for unknown service', async () => {
|
|
414
|
+
const deps = createMockDependencies();
|
|
415
|
+
await runCommand(['auth', 'set', 'unknown-service', '-H', 'X-Token: secret'], deps);
|
|
416
|
+
expect(exitCode).toBe(1);
|
|
417
|
+
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
it('should overwrite existing credentials', async () => {
|
|
420
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
421
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
422
|
+
slack: { objectType: 'slack', token: 'old-token', dCookie: 'old-cookie' },
|
|
423
|
+
}));
|
|
424
|
+
const deps = createMockDependencies({
|
|
425
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
426
|
+
});
|
|
427
|
+
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: new-secret'], deps);
|
|
428
|
+
expect(logs).toContain('Credentials stored.');
|
|
429
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
430
|
+
expect(storedData.slack).toEqual({
|
|
431
|
+
objectType: 'rawCurl',
|
|
432
|
+
curlArguments: ['-H', 'X-Token: new-secret'],
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
337
436
|
describe('curl command', () => {
|
|
338
437
|
it('should pass arguments to subprocess', async () => {
|
|
339
438
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -353,6 +452,18 @@ describe('CLI commands with dependency injection', () => {
|
|
|
353
452
|
]);
|
|
354
453
|
expect(exitCode).toBe(0);
|
|
355
454
|
});
|
|
455
|
+
it('should pass raw curl credentials to subprocess', async () => {
|
|
456
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
457
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
458
|
+
slack: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Custom: header'] },
|
|
459
|
+
}));
|
|
460
|
+
const deps = createMockDependencies({
|
|
461
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
462
|
+
});
|
|
463
|
+
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
464
|
+
expect(capturedArgs).toEqual(['-H', 'X-Custom: header', 'https://slack.com/api/test']);
|
|
465
|
+
expect(exitCode).toBe(0);
|
|
466
|
+
});
|
|
356
467
|
it('should pass multiple arguments correctly', async () => {
|
|
357
468
|
const storePath = join(tempDir, 'credentials.json');
|
|
358
469
|
writeSecureFile(storePath, JSON.stringify({
|
|
@@ -438,42 +549,62 @@ describe('CLI commands with dependency injection', () => {
|
|
|
438
549
|
expect(mockLogin).not.toHaveBeenCalled();
|
|
439
550
|
expect(capturedArgs).toContain('Authorization: Bearer stored-token');
|
|
440
551
|
});
|
|
441
|
-
it('should
|
|
552
|
+
it('should return error when no credentials in store', async () => {
|
|
442
553
|
const storePath = join(tempDir, 'credentials.json');
|
|
443
|
-
const browserStatePath = join(tempDir, 'browser_state.json');
|
|
444
|
-
const configPath = join(tempDir, 'config.json');
|
|
445
|
-
const fakeBrowserPath = join(tempDir, 'fake-browser');
|
|
446
554
|
writeSecureFile(storePath, '{}');
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
555
|
+
const deps = createMockDependencies({
|
|
556
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
557
|
+
});
|
|
558
|
+
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
559
|
+
expect(exitCode).toBe(1);
|
|
560
|
+
expect(errorLogs.some((log) => log.includes('No credentials found for slack'))).toBe(true);
|
|
561
|
+
expect(errorLogs.some((log) => log.includes('auth browser'))).toBe(true);
|
|
562
|
+
expect(errorLogs.some((log) => log.includes('auth set'))).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
it('should work when service does not have getSession but credentials exist', async () => {
|
|
565
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
566
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
567
|
+
nologin: { objectType: 'rawCurl', curlArguments: ['-H', 'X-API-Key: secret'] },
|
|
568
|
+
}));
|
|
569
|
+
const noLoginService = {
|
|
570
|
+
name: 'nologin',
|
|
571
|
+
displayName: 'No Login Service',
|
|
572
|
+
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
573
|
+
loginUrl: 'https://nologin.example.com',
|
|
574
|
+
info: 'A service without browser login support.',
|
|
575
|
+
credentialCheckCurlArguments: [],
|
|
576
|
+
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
|
|
577
|
+
// No getSession - service doesn't support browser login
|
|
578
|
+
};
|
|
579
|
+
const deps = createMockDependencies({
|
|
580
|
+
registry: new Registry([noLoginService]),
|
|
581
|
+
config: createMockConfig({ credentialStorePath: storePath }),
|
|
582
|
+
});
|
|
583
|
+
await runCommand(['curl', 'https://nologin.example.com/api/test'], deps);
|
|
584
|
+
expect(exitCode).toBe(0);
|
|
585
|
+
expect(capturedArgs).toContain('-H');
|
|
586
|
+
expect(capturedArgs).toContain('X-API-Key: secret');
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
describe('auth browser command', () => {
|
|
590
|
+
it('should return error when service does not support browser login', async () => {
|
|
591
|
+
const noLoginService = {
|
|
592
|
+
name: 'nologin',
|
|
593
|
+
displayName: 'No Login Service',
|
|
594
|
+
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
595
|
+
loginUrl: 'https://nologin.example.com',
|
|
596
|
+
info: 'A service without browser login support.',
|
|
466
597
|
credentialCheckCurlArguments: [],
|
|
467
598
|
checkApiCredentials: vi.fn(),
|
|
468
|
-
getSession
|
|
599
|
+
// No getSession - service doesn't support browser login
|
|
469
600
|
};
|
|
470
601
|
const deps = createMockDependencies({
|
|
471
|
-
registry: new Registry([
|
|
472
|
-
config: createMockConfig({ credentialStorePath: storePath, browserStatePath, configPath }),
|
|
602
|
+
registry: new Registry([noLoginService]),
|
|
473
603
|
});
|
|
474
|
-
await runCommand(['
|
|
475
|
-
expect(
|
|
476
|
-
|
|
604
|
+
await runCommand(['auth', 'browser', 'nologin'], deps);
|
|
605
|
+
expect(exitCode).toBe(1);
|
|
606
|
+
const expectedMessage = new BrowserFlowsNotSupportedError('nologin').message;
|
|
607
|
+
expect(errorLogs.some((log) => log.includes(expectedMessage))).toBe(true);
|
|
477
608
|
});
|
|
478
609
|
});
|
|
479
610
|
});
|
|
@@ -508,38 +639,23 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
508
639
|
expect(result.stderr).toContain('No service matches URL');
|
|
509
640
|
expect(result.stderr).toContain('https://unknown-api.example.com');
|
|
510
641
|
});
|
|
511
|
-
|
|
512
|
-
describe('status command', () => {
|
|
513
|
-
it('should return missing when no credentials are stored', () => {
|
|
642
|
+
it('should return error when no credentials exist', () => {
|
|
514
643
|
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
515
|
-
const result = runCli(['
|
|
516
|
-
expect(result.exitCode).toBe(0);
|
|
517
|
-
expect(result.stdout.trim()).toBe('missing');
|
|
518
|
-
});
|
|
519
|
-
it('should return error for unknown service', () => {
|
|
520
|
-
const result = runCli(['status', 'unknown-service'], testEnv);
|
|
644
|
+
const result = runCli(['curl', 'https://slack.com/api/test'], testEnv);
|
|
521
645
|
expect(result.exitCode).toBe(1);
|
|
522
|
-
expect(result.stderr).toContain('
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
526
|
-
const result = runCli(['status'], testEnv);
|
|
527
|
-
expect(result.exitCode).toBe(0);
|
|
528
|
-
const lines = result.stdout.trim().split('\n');
|
|
529
|
-
expect(lines.length).toBeGreaterThan(0);
|
|
530
|
-
expect(lines.some((line) => line.includes('slack: missing'))).toBe(true);
|
|
531
|
-
expect(lines.some((line) => line.includes('discord: missing'))).toBe(true);
|
|
532
|
-
expect(lines.some((line) => line.includes('github: missing'))).toBe(true);
|
|
646
|
+
expect(result.stderr).toContain('No credentials found for slack');
|
|
647
|
+
expect(result.stderr).toContain('auth browser');
|
|
648
|
+
expect(result.stderr).toContain('auth set');
|
|
533
649
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
expect(
|
|
542
|
-
expect(
|
|
650
|
+
});
|
|
651
|
+
describe('auth browser command', () => {
|
|
652
|
+
it('should return error when browser is disabled via LATCHKEY_DISABLE_BROWSER', () => {
|
|
653
|
+
const result = runCli(['auth', 'browser', 'slack'], {
|
|
654
|
+
...testEnv,
|
|
655
|
+
LATCHKEY_DISABLE_BROWSER: '1',
|
|
656
|
+
});
|
|
657
|
+
expect(result.exitCode).toBe(1);
|
|
658
|
+
expect(result.stderr).toContain('Browser is disabled');
|
|
543
659
|
});
|
|
544
660
|
});
|
|
545
661
|
describe('clear command', () => {
|
|
@@ -547,7 +663,7 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
547
663
|
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({
|
|
548
664
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
549
665
|
}));
|
|
550
|
-
const result = runCli(['clear', 'slack'], testEnv);
|
|
666
|
+
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
551
667
|
expect(result.exitCode).toBe(0);
|
|
552
668
|
expect(result.stdout).toContain('API credentials for slack have been cleared');
|
|
553
669
|
const storedData = JSON.parse(readSecureFile(testEnv.LATCHKEY_STORE) ?? '{}');
|
|
@@ -555,12 +671,12 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
555
671
|
});
|
|
556
672
|
it('should report no credentials found when service has no stored credentials', () => {
|
|
557
673
|
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
558
|
-
const result = runCli(['clear', 'slack'], testEnv);
|
|
674
|
+
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
559
675
|
expect(result.exitCode).toBe(0);
|
|
560
676
|
expect(result.stdout).toContain('No API credentials found for slack');
|
|
561
677
|
});
|
|
562
678
|
it('should return error for unknown service', () => {
|
|
563
|
-
const result = runCli(['clear', 'unknown-service'], testEnv);
|
|
679
|
+
const result = runCli(['auth', 'clear', 'unknown-service'], testEnv);
|
|
564
680
|
expect(result.exitCode).toBe(1);
|
|
565
681
|
expect(result.stderr).toContain('Unknown service: unknown-service');
|
|
566
682
|
});
|
|
@@ -569,7 +685,7 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
569
685
|
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
570
686
|
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
571
687
|
}));
|
|
572
|
-
const result = runCli(['clear', 'slack'], testEnv);
|
|
688
|
+
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
573
689
|
expect(result.exitCode).toBe(0);
|
|
574
690
|
const storedData = JSON.parse(readSecureFile(testEnv.LATCHKEY_STORE) ?? '{}');
|
|
575
691
|
expect(storedData.slack).toBeUndefined();
|
|
@@ -579,7 +695,7 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
579
695
|
it('should delete both store and browser state with -y flag', () => {
|
|
580
696
|
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
581
697
|
writeSecureFile(testEnv.LATCHKEY_BROWSER_STATE, '{}');
|
|
582
|
-
const result = runCli(['clear', '-y'], testEnv);
|
|
698
|
+
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
583
699
|
expect(result.exitCode).toBe(0);
|
|
584
700
|
expect(existsSync(testEnv.LATCHKEY_STORE)).toBe(false);
|
|
585
701
|
expect(existsSync(testEnv.LATCHKEY_BROWSER_STATE)).toBe(false);
|
|
@@ -589,23 +705,43 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
589
705
|
it('should delete only existing files with -y flag', () => {
|
|
590
706
|
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
591
707
|
// browser_state does not exist
|
|
592
|
-
const result = runCli(['clear', '-y'], testEnv);
|
|
708
|
+
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
593
709
|
expect(result.exitCode).toBe(0);
|
|
594
710
|
expect(existsSync(testEnv.LATCHKEY_STORE)).toBe(false);
|
|
595
711
|
expect(result.stdout).toContain(`Deleted credentials store: ${testEnv.LATCHKEY_STORE}`);
|
|
596
712
|
expect(result.stdout).not.toContain('browser state');
|
|
597
713
|
});
|
|
598
714
|
it('should report no files to delete when none exist', () => {
|
|
599
|
-
const result = runCli(['clear', '-y'], testEnv);
|
|
715
|
+
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
600
716
|
expect(result.exitCode).toBe(0);
|
|
601
717
|
expect(result.stdout).toContain('No files to delete');
|
|
602
718
|
});
|
|
603
719
|
});
|
|
604
|
-
describe('
|
|
605
|
-
it('should list
|
|
606
|
-
|
|
720
|
+
describe('auth list command', () => {
|
|
721
|
+
it('should list stored credentials as beautified JSON', () => {
|
|
722
|
+
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({
|
|
723
|
+
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
724
|
+
}));
|
|
725
|
+
const result = runCli(['auth', 'list'], testEnv);
|
|
726
|
+
expect(result.exitCode).toBe(0);
|
|
727
|
+
const entries = JSON.parse(result.stdout);
|
|
728
|
+
expect(entries.slack).toBeDefined();
|
|
729
|
+
expect(entries.slack?.credentialType).toBe('slack');
|
|
730
|
+
expect(entries.slack?.credentialStatus).toEqual(expect.any(String));
|
|
731
|
+
});
|
|
732
|
+
it('should output empty object when no credentials are stored', () => {
|
|
733
|
+
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
734
|
+
const result = runCli(['auth', 'list'], testEnv);
|
|
735
|
+
expect(result.exitCode).toBe(0);
|
|
736
|
+
const entries = JSON.parse(result.stdout);
|
|
737
|
+
expect(Object.keys(entries)).toHaveLength(0);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
describe('services list command', () => {
|
|
741
|
+
it('should list all services as JSON', () => {
|
|
742
|
+
const result = runCli(['services', 'list'], testEnv);
|
|
607
743
|
expect(result.exitCode).toBe(0);
|
|
608
|
-
const services = result.stdout.trim()
|
|
744
|
+
const services = JSON.parse(result.stdout.trim());
|
|
609
745
|
expect(services).toContain('slack');
|
|
610
746
|
expect(services).toContain('discord');
|
|
611
747
|
expect(services).toContain('github');
|
|
@@ -613,19 +749,25 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
613
749
|
expect(services).toContain('linear');
|
|
614
750
|
});
|
|
615
751
|
});
|
|
616
|
-
describe('info command', () => {
|
|
617
|
-
it('should show
|
|
618
|
-
|
|
752
|
+
describe('services info command', () => {
|
|
753
|
+
it('should show login options, credentials status, and developer notes as JSON', () => {
|
|
754
|
+
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
755
|
+
const result = runCli(['services', 'info', 'slack'], testEnv);
|
|
619
756
|
expect(result.exitCode).toBe(0);
|
|
620
|
-
|
|
757
|
+
const info = JSON.parse(result.stdout);
|
|
758
|
+
expect(info.authOptions).toEqual(['browser', 'set']);
|
|
759
|
+
expect(info.credentialStatus).toBe('missing');
|
|
760
|
+
expect(info.developerNotes).toEqual(expect.any(String));
|
|
621
761
|
});
|
|
622
|
-
it('should show
|
|
623
|
-
|
|
762
|
+
it('should show auth set only for services without browser login', () => {
|
|
763
|
+
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
764
|
+
const result = runCli(['services', 'info', 'mailchimp'], testEnv);
|
|
624
765
|
expect(result.exitCode).toBe(0);
|
|
625
|
-
|
|
766
|
+
const info = JSON.parse(result.stdout);
|
|
767
|
+
expect(info.authOptions).toEqual(['set']);
|
|
626
768
|
});
|
|
627
769
|
it('should return error for unknown service', () => {
|
|
628
|
-
const result = runCli(['info', 'unknown-service'], testEnv);
|
|
770
|
+
const result = runCli(['services', 'info', 'unknown-service'], testEnv);
|
|
629
771
|
expect(result.exitCode).toBe(1);
|
|
630
772
|
expect(result.stderr).toContain('Unknown service');
|
|
631
773
|
});
|