latchkey 0.1.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +93 -56
- package/dist/integrations/SKILL.md +77 -0
- package/dist/package.json +67 -0
- package/dist/scripts/encryptFile.d.ts +21 -0
- package/dist/scripts/encryptFile.d.ts.map +1 -0
- package/dist/scripts/encryptFile.js +101 -0
- package/dist/scripts/encryptFile.js.map +1 -0
- 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 +116 -1
- package/dist/src/apiCredentials.d.ts.map +1 -1
- package/dist/src/apiCredentials.js +119 -1
- package/dist/src/apiCredentials.js.map +1 -1
- package/dist/src/browserState.d.ts +8 -0
- package/dist/src/browserState.d.ts.map +1 -0
- package/dist/src/browserState.js +21 -0
- package/dist/src/browserState.js.map +1 -0
- package/dist/src/cli.js +5 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +218 -81
- 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 +50 -4
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauthUtils.d.ts +49 -0
- package/dist/src/oauthUtils.d.ts.map +1 -0
- package/dist/src/oauthUtils.js +183 -0
- package/dist/src/oauthUtils.js.map +1 -0
- package/dist/src/playwrightUtils.d.ts +14 -1
- package/dist/src/playwrightUtils.d.ts.map +1 -1
- package/dist/src/playwrightUtils.js +37 -8
- 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 +43 -15
- package/dist/src/services/base.d.ts.map +1 -1
- package/dist/src/services/base.js +49 -9
- package/dist/src/services/base.js.map +1 -1
- package/dist/src/services/discord.d.ts +4 -3
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +6 -22
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +5 -4
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +10 -27
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/github.d.ts +5 -4
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +21 -30
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/google.d.ts +34 -0
- package/dist/src/services/google.d.ts.map +1 -0
- package/dist/src/services/google.js +336 -0
- package/dist/src/services/google.js.map +1 -0
- package/dist/src/services/index.d.ts +4 -2
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +4 -1
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/linear.d.ts +5 -4
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +10 -29
- 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 +29 -0
- package/dist/src/services/notion.d.ts.map +1 -0
- package/dist/src/services/notion.js +102 -0
- package/dist/src/services/notion.js.map +1 -0
- package/dist/src/services/slack.d.ts +3 -1
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +5 -5
- package/dist/src/services/slack.js.map +1 -1
- package/dist/src/skillMd.d.ts +2 -0
- package/dist/src/skillMd.d.ts.map +1 -0
- package/dist/src/skillMd.js +19 -0
- package/dist/src/skillMd.js.map +1 -0
- package/dist/tests/apiCredentials.test.js +59 -1
- package/dist/tests/apiCredentials.test.js.map +1 -1
- package/dist/tests/cli.test.js +283 -104
- 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 +28 -3
- 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 +6 -6
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,14 +123,18 @@ 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 = {}) {
|
|
129
132
|
const mockSlackService = {
|
|
130
133
|
name: 'slack',
|
|
134
|
+
displayName: 'Slack',
|
|
131
135
|
baseApiUrls: ['https://slack.com/api/'],
|
|
132
136
|
loginUrl: 'https://slack.com/signin',
|
|
137
|
+
info: 'Test info for Slack service.',
|
|
133
138
|
credentialCheckCurlArguments: ['https://slack.com/api/auth.test'],
|
|
134
139
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
|
|
135
140
|
getSession: vi.fn().mockReturnValue({
|
|
@@ -182,53 +187,60 @@ describe('CLI commands with dependency injection', () => {
|
|
|
182
187
|
afterEach(() => {
|
|
183
188
|
rmSync(tempDir, { recursive: true, force: true });
|
|
184
189
|
});
|
|
185
|
-
describe('services command', () => {
|
|
186
|
-
it('should list all services as
|
|
190
|
+
describe('services list command', () => {
|
|
191
|
+
it('should list all services as JSON', async () => {
|
|
187
192
|
const deps = createMockDependencies();
|
|
188
|
-
await runCommand(['services'], deps);
|
|
193
|
+
await runCommand(['services', 'list'], deps);
|
|
189
194
|
expect(logs).toHaveLength(1);
|
|
190
|
-
const services = (logs[0] ?? '')
|
|
195
|
+
const services = JSON.parse(logs[0] ?? '');
|
|
191
196
|
expect(services).toContain('slack');
|
|
192
197
|
});
|
|
193
198
|
});
|
|
194
|
-
describe('
|
|
195
|
-
it('should
|
|
199
|
+
describe('services info command', () => {
|
|
200
|
+
it('should show login options, credentials status, and developer notes', async () => {
|
|
196
201
|
const storePath = join(tempDir, 'credentials.json');
|
|
197
202
|
writeSecureFile(storePath, '{}');
|
|
198
203
|
const deps = createMockDependencies({
|
|
199
204
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
200
205
|
});
|
|
201
|
-
await runCommand(['
|
|
202
|
-
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.');
|
|
203
212
|
});
|
|
204
|
-
it('should
|
|
213
|
+
it('should show auth set only for services without browser login', async () => {
|
|
205
214
|
const storePath = join(tempDir, 'credentials.json');
|
|
206
|
-
writeSecureFile(storePath,
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
};
|
|
209
225
|
const deps = createMockDependencies({
|
|
226
|
+
registry: new Registry([noLoginService]),
|
|
210
227
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
211
228
|
});
|
|
212
|
-
await runCommand(['
|
|
213
|
-
|
|
229
|
+
await runCommand(['services', 'info', 'nologin'], deps);
|
|
230
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
231
|
+
expect(info.authOptions).toEqual(['set']);
|
|
214
232
|
});
|
|
215
|
-
it('should
|
|
216
|
-
const deps = createMockDependencies();
|
|
217
|
-
await runCommand(['status', 'unknown-service'], deps);
|
|
218
|
-
expect(exitCode).toBe(1);
|
|
219
|
-
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
220
|
-
});
|
|
221
|
-
it('should return status for all services when no service name provided', async () => {
|
|
233
|
+
it('should not list browser in authOptions when LATCHKEY_DISABLE_BROWSER is in effect', async () => {
|
|
222
234
|
const storePath = join(tempDir, 'credentials.json');
|
|
223
235
|
writeSecureFile(storePath, '{}');
|
|
224
236
|
const deps = createMockDependencies({
|
|
225
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
237
|
+
config: createMockConfig({ credentialStorePath: storePath, browserDisabled: true }),
|
|
226
238
|
});
|
|
227
|
-
await runCommand(['
|
|
228
|
-
|
|
229
|
-
expect(
|
|
239
|
+
await runCommand(['services', 'info', 'slack'], deps);
|
|
240
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
241
|
+
expect(info.authOptions).toEqual(['set']);
|
|
230
242
|
});
|
|
231
|
-
it('should
|
|
243
|
+
it('should show valid credentials status when credentials are valid', async () => {
|
|
232
244
|
const storePath = join(tempDir, 'credentials.json');
|
|
233
245
|
writeSecureFile(storePath, JSON.stringify({
|
|
234
246
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
@@ -236,9 +248,15 @@ describe('CLI commands with dependency injection', () => {
|
|
|
236
248
|
const deps = createMockDependencies({
|
|
237
249
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
238
250
|
});
|
|
239
|
-
await runCommand(['
|
|
240
|
-
|
|
241
|
-
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);
|
|
242
260
|
});
|
|
243
261
|
});
|
|
244
262
|
describe('clear command', () => {
|
|
@@ -250,7 +268,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
250
268
|
const deps = createMockDependencies({
|
|
251
269
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
252
270
|
});
|
|
253
|
-
await runCommand(['clear', 'slack'], deps);
|
|
271
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
254
272
|
expect(logs.some((log) => log.includes('have been cleared'))).toBe(true);
|
|
255
273
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
256
274
|
expect(storedData.slack).toBeUndefined();
|
|
@@ -261,7 +279,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
261
279
|
const deps = createMockDependencies({
|
|
262
280
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
263
281
|
});
|
|
264
|
-
await runCommand(['clear', 'slack'], deps);
|
|
282
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
265
283
|
expect(logs.some((log) => log.includes('No API credentials found'))).toBe(true);
|
|
266
284
|
});
|
|
267
285
|
it('should return error for unknown service', async () => {
|
|
@@ -269,13 +287,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
269
287
|
const deps = createMockDependencies({
|
|
270
288
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
271
289
|
});
|
|
272
|
-
await runCommand(['clear', 'unknown-service'], deps);
|
|
290
|
+
await runCommand(['auth', 'clear', 'unknown-service'], deps);
|
|
273
291
|
expect(exitCode).toBe(1);
|
|
274
292
|
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
275
293
|
});
|
|
276
294
|
it('should use default config paths', async () => {
|
|
277
295
|
const deps = createMockDependencies();
|
|
278
|
-
await runCommand(['clear', 'slack'], deps);
|
|
296
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
279
297
|
// With default paths, should report no credentials found (not error about missing env var)
|
|
280
298
|
expect(logs.some((log) => log.includes('No API credentials found'))).toBe(true);
|
|
281
299
|
});
|
|
@@ -288,7 +306,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
288
306
|
const deps = createMockDependencies({
|
|
289
307
|
config: createMockConfig({ credentialStorePath: storePath }),
|
|
290
308
|
});
|
|
291
|
-
await runCommand(['clear', 'slack'], deps);
|
|
309
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
292
310
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
293
311
|
expect(storedData.slack).toBeUndefined();
|
|
294
312
|
expect(storedData.discord).toBeDefined();
|
|
@@ -302,7 +320,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
302
320
|
const deps = createMockDependencies({
|
|
303
321
|
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
304
322
|
});
|
|
305
|
-
await runCommand(['clear', '-y'], deps);
|
|
323
|
+
await runCommand(['auth', 'clear', '-y'], deps);
|
|
306
324
|
expect(existsSync(storePath)).toBe(false);
|
|
307
325
|
expect(existsSync(browserStatePath)).toBe(false);
|
|
308
326
|
expect(logs.some((log) => log.includes('Deleted credentials store'))).toBe(true);
|
|
@@ -314,10 +332,107 @@ describe('CLI commands with dependency injection', () => {
|
|
|
314
332
|
const deps = createMockDependencies({
|
|
315
333
|
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
316
334
|
});
|
|
317
|
-
await runCommand(['clear', '-y'], deps);
|
|
335
|
+
await runCommand(['auth', 'clear', '-y'], deps);
|
|
318
336
|
expect(logs.some((log) => log.includes('No files to delete'))).toBe(true);
|
|
319
337
|
});
|
|
320
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
|
+
});
|
|
321
436
|
describe('curl command', () => {
|
|
322
437
|
it('should pass arguments to subprocess', async () => {
|
|
323
438
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -337,6 +452,18 @@ describe('CLI commands with dependency injection', () => {
|
|
|
337
452
|
]);
|
|
338
453
|
expect(exitCode).toBe(0);
|
|
339
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
|
+
});
|
|
340
467
|
it('should pass multiple arguments correctly', async () => {
|
|
341
468
|
const storePath = join(tempDir, 'credentials.json');
|
|
342
469
|
writeSecureFile(storePath, JSON.stringify({
|
|
@@ -406,8 +533,10 @@ describe('CLI commands with dependency injection', () => {
|
|
|
406
533
|
const mockLogin = vi.fn();
|
|
407
534
|
const mockSlackService = {
|
|
408
535
|
name: 'slack',
|
|
536
|
+
displayName: 'Slack',
|
|
409
537
|
baseApiUrls: ['https://slack.com/api/'],
|
|
410
538
|
loginUrl: 'https://slack.com/signin',
|
|
539
|
+
info: 'Test info for Slack service.',
|
|
411
540
|
credentialCheckCurlArguments: [],
|
|
412
541
|
checkApiCredentials: vi.fn(),
|
|
413
542
|
getSession: vi.fn().mockReturnValue({ login: mockLogin }),
|
|
@@ -420,40 +549,62 @@ describe('CLI commands with dependency injection', () => {
|
|
|
420
549
|
expect(mockLogin).not.toHaveBeenCalled();
|
|
421
550
|
expect(capturedArgs).toContain('Authorization: Bearer stored-token');
|
|
422
551
|
});
|
|
423
|
-
it('should
|
|
552
|
+
it('should return error when no credentials in store', async () => {
|
|
424
553
|
const storePath = join(tempDir, 'credentials.json');
|
|
425
|
-
const browserStatePath = join(tempDir, 'browser_state.json');
|
|
426
|
-
const configPath = join(tempDir, 'config.json');
|
|
427
|
-
const fakeBrowserPath = join(tempDir, 'fake-browser');
|
|
428
554
|
writeSecureFile(storePath, '{}');
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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.',
|
|
446
597
|
credentialCheckCurlArguments: [],
|
|
447
598
|
checkApiCredentials: vi.fn(),
|
|
448
|
-
getSession
|
|
599
|
+
// No getSession - service doesn't support browser login
|
|
449
600
|
};
|
|
450
601
|
const deps = createMockDependencies({
|
|
451
|
-
registry: new Registry([
|
|
452
|
-
config: createMockConfig({ credentialStorePath: storePath, browserStatePath, configPath }),
|
|
602
|
+
registry: new Registry([noLoginService]),
|
|
453
603
|
});
|
|
454
|
-
await runCommand(['
|
|
455
|
-
expect(
|
|
456
|
-
|
|
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);
|
|
457
608
|
});
|
|
458
609
|
});
|
|
459
610
|
});
|
|
@@ -488,38 +639,23 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
488
639
|
expect(result.stderr).toContain('No service matches URL');
|
|
489
640
|
expect(result.stderr).toContain('https://unknown-api.example.com');
|
|
490
641
|
});
|
|
491
|
-
|
|
492
|
-
describe('status command', () => {
|
|
493
|
-
it('should return missing when no credentials are stored', () => {
|
|
642
|
+
it('should return error when no credentials exist', () => {
|
|
494
643
|
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
495
|
-
const result = runCli(['
|
|
496
|
-
expect(result.exitCode).toBe(0);
|
|
497
|
-
expect(result.stdout.trim()).toBe('missing');
|
|
498
|
-
});
|
|
499
|
-
it('should return error for unknown service', () => {
|
|
500
|
-
const result = runCli(['status', 'unknown-service'], testEnv);
|
|
644
|
+
const result = runCli(['curl', 'https://slack.com/api/test'], testEnv);
|
|
501
645
|
expect(result.exitCode).toBe(1);
|
|
502
|
-
expect(result.stderr).toContain('
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
506
|
-
const result = runCli(['status'], testEnv);
|
|
507
|
-
expect(result.exitCode).toBe(0);
|
|
508
|
-
const lines = result.stdout.trim().split('\n');
|
|
509
|
-
expect(lines.length).toBeGreaterThan(0);
|
|
510
|
-
expect(lines.some((line) => line.includes('slack: missing'))).toBe(true);
|
|
511
|
-
expect(lines.some((line) => line.includes('discord: missing'))).toBe(true);
|
|
512
|
-
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');
|
|
513
649
|
});
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
expect(
|
|
522
|
-
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');
|
|
523
659
|
});
|
|
524
660
|
});
|
|
525
661
|
describe('clear command', () => {
|
|
@@ -527,7 +663,7 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
527
663
|
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({
|
|
528
664
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
529
665
|
}));
|
|
530
|
-
const result = runCli(['clear', 'slack'], testEnv);
|
|
666
|
+
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
531
667
|
expect(result.exitCode).toBe(0);
|
|
532
668
|
expect(result.stdout).toContain('API credentials for slack have been cleared');
|
|
533
669
|
const storedData = JSON.parse(readSecureFile(testEnv.LATCHKEY_STORE) ?? '{}');
|
|
@@ -535,12 +671,12 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
535
671
|
});
|
|
536
672
|
it('should report no credentials found when service has no stored credentials', () => {
|
|
537
673
|
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
538
|
-
const result = runCli(['clear', 'slack'], testEnv);
|
|
674
|
+
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
539
675
|
expect(result.exitCode).toBe(0);
|
|
540
676
|
expect(result.stdout).toContain('No API credentials found for slack');
|
|
541
677
|
});
|
|
542
678
|
it('should return error for unknown service', () => {
|
|
543
|
-
const result = runCli(['clear', 'unknown-service'], testEnv);
|
|
679
|
+
const result = runCli(['auth', 'clear', 'unknown-service'], testEnv);
|
|
544
680
|
expect(result.exitCode).toBe(1);
|
|
545
681
|
expect(result.stderr).toContain('Unknown service: unknown-service');
|
|
546
682
|
});
|
|
@@ -549,7 +685,7 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
549
685
|
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
550
686
|
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
551
687
|
}));
|
|
552
|
-
const result = runCli(['clear', 'slack'], testEnv);
|
|
688
|
+
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
553
689
|
expect(result.exitCode).toBe(0);
|
|
554
690
|
const storedData = JSON.parse(readSecureFile(testEnv.LATCHKEY_STORE) ?? '{}');
|
|
555
691
|
expect(storedData.slack).toBeUndefined();
|
|
@@ -559,7 +695,7 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
559
695
|
it('should delete both store and browser state with -y flag', () => {
|
|
560
696
|
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
561
697
|
writeSecureFile(testEnv.LATCHKEY_BROWSER_STATE, '{}');
|
|
562
|
-
const result = runCli(['clear', '-y'], testEnv);
|
|
698
|
+
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
563
699
|
expect(result.exitCode).toBe(0);
|
|
564
700
|
expect(existsSync(testEnv.LATCHKEY_STORE)).toBe(false);
|
|
565
701
|
expect(existsSync(testEnv.LATCHKEY_BROWSER_STATE)).toBe(false);
|
|
@@ -569,23 +705,43 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
569
705
|
it('should delete only existing files with -y flag', () => {
|
|
570
706
|
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
571
707
|
// browser_state does not exist
|
|
572
|
-
const result = runCli(['clear', '-y'], testEnv);
|
|
708
|
+
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
573
709
|
expect(result.exitCode).toBe(0);
|
|
574
710
|
expect(existsSync(testEnv.LATCHKEY_STORE)).toBe(false);
|
|
575
711
|
expect(result.stdout).toContain(`Deleted credentials store: ${testEnv.LATCHKEY_STORE}`);
|
|
576
712
|
expect(result.stdout).not.toContain('browser state');
|
|
577
713
|
});
|
|
578
714
|
it('should report no files to delete when none exist', () => {
|
|
579
|
-
const result = runCli(['clear', '-y'], testEnv);
|
|
715
|
+
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
580
716
|
expect(result.exitCode).toBe(0);
|
|
581
717
|
expect(result.stdout).toContain('No files to delete');
|
|
582
718
|
});
|
|
583
719
|
});
|
|
584
|
-
describe('
|
|
585
|
-
it('should list
|
|
586
|
-
|
|
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);
|
|
587
743
|
expect(result.exitCode).toBe(0);
|
|
588
|
-
const services = result.stdout.trim()
|
|
744
|
+
const services = JSON.parse(result.stdout.trim());
|
|
589
745
|
expect(services).toContain('slack');
|
|
590
746
|
expect(services).toContain('discord');
|
|
591
747
|
expect(services).toContain('github');
|
|
@@ -593,5 +749,28 @@ describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
|
593
749
|
expect(services).toContain('linear');
|
|
594
750
|
});
|
|
595
751
|
});
|
|
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);
|
|
756
|
+
expect(result.exitCode).toBe(0);
|
|
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));
|
|
761
|
+
});
|
|
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);
|
|
765
|
+
expect(result.exitCode).toBe(0);
|
|
766
|
+
const info = JSON.parse(result.stdout);
|
|
767
|
+
expect(info.authOptions).toEqual(['set']);
|
|
768
|
+
});
|
|
769
|
+
it('should return error for unknown service', () => {
|
|
770
|
+
const result = runCli(['services', 'info', 'unknown-service'], testEnv);
|
|
771
|
+
expect(result.exitCode).toBe(1);
|
|
772
|
+
expect(result.stderr).toContain('Unknown service');
|
|
773
|
+
});
|
|
774
|
+
});
|
|
596
775
|
});
|
|
597
776
|
//# sourceMappingURL=cli.test.js.map
|