latchkey 2.6.0 → 2.7.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/README.md +63 -7
- package/dist/scripts/recordBrowserSession.js +3 -3
- package/dist/scripts/recordBrowserSession.js.map +1 -1
- package/dist/src/{apiCredentials.d.ts → apiCredentials/base.d.ts} +6 -6
- package/dist/src/apiCredentials/base.d.ts.map +1 -0
- package/dist/src/{apiCredentials.js → apiCredentials/base.js} +5 -5
- package/dist/src/apiCredentials/base.js.map +1 -0
- package/dist/src/{apiCredentialsSerialization.d.ts → apiCredentials/serialization.d.ts} +5 -5
- package/dist/src/apiCredentials/serialization.d.ts.map +1 -0
- package/dist/src/{apiCredentialsSerialization.js → apiCredentials/serialization.js} +9 -9
- package/dist/src/apiCredentials/serialization.js.map +1 -0
- package/dist/src/{apiCredentialStore.d.ts → apiCredentials/store.d.ts} +3 -3
- package/dist/src/apiCredentials/store.d.ts.map +1 -0
- package/dist/src/{apiCredentialStore.js → apiCredentials/store.js} +2 -2
- package/dist/src/apiCredentials/store.js.map +1 -0
- package/dist/src/apiCredentials/utils.d.ts +13 -0
- package/dist/src/apiCredentials/utils.d.ts.map +1 -0
- package/dist/src/apiCredentials/utils.js +27 -0
- package/dist/src/apiCredentials/utils.js.map +1 -0
- package/dist/src/cli.js +42 -39
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts +6 -3
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +243 -190
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +36 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +112 -17
- package/dist/src/config.js.map +1 -1
- package/dist/src/configDataStore.d.ts +44 -0
- package/dist/src/configDataStore.d.ts.map +1 -1
- package/dist/src/configDataStore.js +27 -0
- package/dist/src/configDataStore.js.map +1 -1
- package/dist/src/curl.d.ts +41 -8
- package/dist/src/curl.d.ts.map +1 -1
- package/dist/src/curl.js +80 -75
- package/dist/src/curl.js.map +1 -1
- package/dist/src/curlInjection.d.ts +46 -0
- package/dist/src/curlInjection.d.ts.map +1 -0
- package/dist/src/curlInjection.js +99 -0
- package/dist/src/curlInjection.js.map +1 -0
- package/dist/src/errorMessages.d.ts +14 -0
- package/dist/src/errorMessages.d.ts.map +1 -0
- package/dist/src/errorMessages.js +22 -0
- package/dist/src/errorMessages.js.map +1 -0
- package/dist/src/gateway/client.d.ts +32 -0
- package/dist/src/gateway/client.d.ts.map +1 -0
- package/dist/src/gateway/client.js +89 -0
- package/dist/src/gateway/client.js.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts +43 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.js +297 -0
- package/dist/src/gateway/gatewayEndpoint.js.map +1 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts +105 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts.map +1 -0
- package/dist/src/gateway/latchkeyEndpoint.js +144 -0
- package/dist/src/gateway/latchkeyEndpoint.js.map +1 -0
- package/dist/src/gateway/server.d.ts +20 -0
- package/dist/src/gateway/server.d.ts.map +1 -0
- package/dist/src/gateway/server.js +90 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/permissions.d.ts +2 -1
- package/dist/src/permissions.d.ts.map +1 -1
- package/dist/src/permissions.js +8 -4
- package/dist/src/permissions.js.map +1 -1
- package/dist/src/{registry.d.ts → serviceRegistry.d.ts} +4 -4
- package/dist/src/serviceRegistry.d.ts.map +1 -0
- package/dist/src/{registry.js → serviceRegistry.js} +4 -4
- package/dist/src/serviceRegistry.js.map +1 -0
- package/dist/src/services/aws.d.ts +2 -2
- package/dist/src/services/aws.d.ts.map +1 -1
- package/dist/src/services/aws.js +17 -10
- package/dist/src/services/aws.js.map +1 -1
- package/dist/src/services/core/base.d.ts +2 -2
- package/dist/src/services/core/base.d.ts.map +1 -1
- package/dist/src/services/core/base.js +3 -3
- package/dist/src/services/core/base.js.map +1 -1
- package/dist/src/services/core/registered.d.ts +2 -2
- package/dist/src/services/core/registered.d.ts.map +1 -1
- package/dist/src/services/core/registered.js +2 -2
- package/dist/src/services/core/registered.js.map +1 -1
- package/dist/src/services/discord.d.ts +1 -1
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +1 -1
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +1 -1
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +1 -1
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/github.d.ts +1 -1
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +1 -1
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/google/base.d.ts +2 -2
- package/dist/src/services/google/base.d.ts.map +1 -1
- package/dist/src/services/google/base.js +3 -3
- package/dist/src/services/google/base.js.map +1 -1
- package/dist/src/services/google/directions.d.ts +1 -1
- package/dist/src/services/google/directions.d.ts.map +1 -1
- package/dist/src/services/linear.d.ts +1 -1
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +1 -1
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/notion.d.ts +1 -1
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +1 -1
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/services/sentry.d.ts +2 -2
- package/dist/src/services/sentry.d.ts.map +1 -1
- package/dist/src/services/sentry.js +6 -3
- package/dist/src/services/sentry.js.map +1 -1
- package/dist/src/services/slack.d.ts +3 -3
- 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/services/telegram.d.ts +2 -2
- package/dist/src/services/telegram.d.ts.map +1 -1
- package/dist/src/services/telegram.js +2 -2
- package/dist/src/services/telegram.js.map +1 -1
- package/dist/src/sharedOperations.d.ts +44 -0
- package/dist/src/sharedOperations.d.ts.map +1 -0
- package/dist/src/sharedOperations.js +131 -0
- package/dist/src/sharedOperations.js.map +1 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +4 -0
- package/dist/src/version.js.map +1 -0
- package/dist/tests/apiCredentialStore.test.js +2 -2
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/apiCredentials.test.js +37 -36
- package/dist/tests/apiCredentials.test.js.map +1 -1
- package/dist/tests/cli.test.js +241 -55
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/config.test.d.ts +2 -0
- package/dist/tests/config.test.d.ts.map +1 -0
- package/dist/tests/config.test.js +150 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/gateway.test.d.ts +2 -0
- package/dist/tests/gateway.test.d.ts.map +1 -0
- package/dist/tests/gateway.test.js +566 -0
- package/dist/tests/gateway.test.js.map +1 -0
- package/dist/tests/gatewayClient.test.d.ts +2 -0
- package/dist/tests/gatewayClient.test.d.ts.map +1 -0
- package/dist/tests/gatewayClient.test.js +85 -0
- package/dist/tests/gatewayClient.test.js.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.d.ts +2 -0
- package/dist/tests/latchkeyEndpoint.test.d.ts.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.js +385 -0
- package/dist/tests/latchkeyEndpoint.test.js.map +1 -0
- package/dist/tests/permissions.test.js +18 -3
- package/dist/tests/permissions.test.js.map +1 -1
- package/dist/tests/serviceRegistry.test.d.ts +2 -0
- package/dist/tests/serviceRegistry.test.d.ts.map +1 -0
- package/dist/tests/{registry.test.js → serviceRegistry.test.js} +17 -17
- package/dist/tests/serviceRegistry.test.js.map +1 -0
- package/dist/tests/servicesAgainstRecordings.test.js +3 -3
- package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
- package/dist/tests/sharedOperations.test.d.ts +2 -0
- package/dist/tests/sharedOperations.test.d.ts.map +1 -0
- package/dist/tests/sharedOperations.test.js +264 -0
- package/dist/tests/sharedOperations.test.js.map +1 -0
- package/package.json +8 -2
- package/dist/src/apiCredentialStore.d.ts.map +0 -1
- package/dist/src/apiCredentialStore.js.map +0 -1
- package/dist/src/apiCredentials.d.ts.map +0 -1
- package/dist/src/apiCredentials.js.map +0 -1
- package/dist/src/apiCredentialsSerialization.d.ts.map +0 -1
- package/dist/src/apiCredentialsSerialization.js.map +0 -1
- package/dist/src/registry.d.ts.map +0 -1
- package/dist/src/registry.js.map +0 -1
- package/dist/tests/registry.test.d.ts +0 -2
- package/dist/tests/registry.test.d.ts.map +0 -1
- package/dist/tests/registry.test.js.map +0 -1
package/dist/src/cliCommands.js
CHANGED
|
@@ -3,55 +3,40 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import { ApiCredentialStore } from './
|
|
7
|
-
import {
|
|
6
|
+
import { ApiCredentialStore } from './apiCredentials/store.js';
|
|
7
|
+
import { RawCurlCredentials } from './apiCredentials/base.js';
|
|
8
|
+
import { CredentialsExpiredError, NoCredentialsForServiceError, NoServiceForUrlError, prepareCurlInvocation, RequestNotPermittedError, UrlExtractionFailedError, } from './curlInjection.js';
|
|
8
9
|
import { BROWSER_SOURCES, BrowserNotFoundError, DEFAULT_BROWSER_SOURCES, ensureBrowser, } from './browserConfig.js';
|
|
9
10
|
import { CONFIG } from './config.js';
|
|
10
|
-
import { deleteRegisteredService,
|
|
11
|
-
import { BrowserDisabledError,
|
|
11
|
+
import { deleteRegisteredService, saveRegisteredService } from './configDataStore.js';
|
|
12
|
+
import { BrowserDisabledError, BrowserFlowsNotSupportedError, GraphicalEnvironmentNotFoundError, } from './playwrightUtils.js';
|
|
12
13
|
import { EncryptedStorage } from './encryptedStorage.js';
|
|
13
|
-
import { DuplicateServiceNameError, InvalidServiceNameError,
|
|
14
|
+
import { DuplicateServiceNameError, InvalidServiceNameError, SERVICE_REGISTRY, canonicalizeServiceName, } from './serviceRegistry.js';
|
|
14
15
|
import { RegisteredService } from './services/core/registered.js';
|
|
15
16
|
import { LoginCancelledError, LoginFailedError, NoCurlCredentialsNotSupportedError, } from './services/index.js';
|
|
16
|
-
import { extractUrlFromCurlArguments, run as curlRun } from './curl.js';
|
|
17
|
+
import { CurlParseError, extractUrlFromCurlArguments, run as curlRun, runAsync as curlRunAsync, } from './curl.js';
|
|
17
18
|
import { checkPermission, PermissionCheckError } from './permissions.js';
|
|
19
|
+
import { ErrorMessages } from './errorMessages.js';
|
|
18
20
|
import { getSkillMdContent } from './skillMd.js';
|
|
21
|
+
import { startGateway } from './gateway/server.js';
|
|
22
|
+
import { callLatchkeyEndpoint, GatewayCommandNotSupportedError, GatewayCurlRewriteError, GatewayRequestError, rewriteCurlArgumentsForGateway, } from './gateway/client.js';
|
|
23
|
+
import { servicesList, servicesInfo, authList, authBrowser, authBrowserPrepare, UnknownServiceError, BrowserNotConfiguredError, PreparationRequiredError, } from './sharedOperations.js';
|
|
24
|
+
import { VERSION } from './version.js';
|
|
19
25
|
/**
|
|
20
26
|
* Exit code used when a request is rejected by permission rules.
|
|
21
27
|
* Uses the Unix convention for "command not permitted" (126).
|
|
22
28
|
* Curl itself does not use this exit code.
|
|
23
29
|
*/
|
|
24
30
|
export const PERMISSION_DENIED_EXIT_CODE = 126;
|
|
25
|
-
/**
|
|
26
|
-
* Try to refresh expired credentials if the service supports it.
|
|
27
|
-
* Returns refreshed credentials if successful, otherwise returns the original credentials.
|
|
28
|
-
*/
|
|
29
|
-
async function maybeRefreshCredentials(service, apiCredentials, apiCredentialStore) {
|
|
30
|
-
if (apiCredentials.isExpired() !== true || !service.refreshCredentials) {
|
|
31
|
-
return apiCredentials;
|
|
32
|
-
}
|
|
33
|
-
const refreshedCredentials = await service.refreshCredentials(apiCredentials);
|
|
34
|
-
if (refreshedCredentials !== null) {
|
|
35
|
-
apiCredentialStore.save(service.name, refreshedCredentials);
|
|
36
|
-
return refreshedCredentials;
|
|
37
|
-
}
|
|
38
|
-
return apiCredentials;
|
|
39
|
-
}
|
|
40
|
-
async function getCredentialStatus(service, credentials, apiCredentialStore) {
|
|
41
|
-
if (credentials === null) {
|
|
42
|
-
return ApiCredentialStatus.Missing;
|
|
43
|
-
}
|
|
44
|
-
const refreshed = await maybeRefreshCredentials(service, credentials, apiCredentialStore);
|
|
45
|
-
return service.checkApiCredentials(refreshed);
|
|
46
|
-
}
|
|
47
31
|
/**
|
|
48
32
|
* Default implementation of CLI dependencies.
|
|
49
33
|
*/
|
|
50
34
|
export function createDefaultDependencies() {
|
|
51
35
|
return {
|
|
52
|
-
registry:
|
|
36
|
+
registry: SERVICE_REGISTRY,
|
|
53
37
|
config: CONFIG,
|
|
54
38
|
runCurl: curlRun,
|
|
39
|
+
runCurlAsync: curlRunAsync,
|
|
55
40
|
checkPermission: checkPermission,
|
|
56
41
|
confirm: defaultConfirm,
|
|
57
42
|
exit: (code) => process.exit(code),
|
|
@@ -61,8 +46,40 @@ export function createDefaultDependencies() {
|
|
|
61
46
|
errorLog: (message) => {
|
|
62
47
|
console.error(message);
|
|
63
48
|
},
|
|
49
|
+
version: VERSION,
|
|
64
50
|
};
|
|
65
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Forward a request to the gateway's `/latchkey/` endpoint. On transport or
|
|
54
|
+
* protocol errors the CLI exits with status 1 after logging the error message.
|
|
55
|
+
*/
|
|
56
|
+
async function forwardToGateway(deps, request) {
|
|
57
|
+
const gatewayUrl = deps.config.gatewayUrl;
|
|
58
|
+
if (gatewayUrl === null) {
|
|
59
|
+
throw new GatewayCommandNotSupportedError(request.command);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return await callLatchkeyEndpoint(gatewayUrl, request);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof GatewayRequestError) {
|
|
66
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
67
|
+
deps.exit(1);
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* If the CLI is running in gateway mode, log an error and exit. Used for
|
|
74
|
+
* commands that manage local state and cannot be meaningfully delegated.
|
|
75
|
+
*/
|
|
76
|
+
function refuseInGatewayMode(deps, commandName) {
|
|
77
|
+
if (deps.config.gatewayUrl !== null) {
|
|
78
|
+
const error = new GatewayCommandNotSupportedError(commandName);
|
|
79
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
80
|
+
deps.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
66
83
|
async function defaultConfirm(message) {
|
|
67
84
|
const readline = createInterface({
|
|
68
85
|
input: process.stdin,
|
|
@@ -136,44 +153,6 @@ async function clearService(deps, serviceName) {
|
|
|
136
153
|
deps.log(`No API credentials found for ${serviceName}.`);
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Check if browser login is disabled via environment variable.
|
|
141
|
-
* Exits with error if LATCHKEY_DISABLE_BROWSER is set.
|
|
142
|
-
*/
|
|
143
|
-
function checkBrowserNotDisabledOrExit(deps) {
|
|
144
|
-
if (deps.config.browserDisabled) {
|
|
145
|
-
deps.errorLog(new BrowserDisabledError().message);
|
|
146
|
-
deps.exit(1);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Check if a graphical environment is available.
|
|
151
|
-
* Exits with error if no display server (X11 or Wayland) is detected on Linux.
|
|
152
|
-
*/
|
|
153
|
-
function checkGraphicalEnvironmentOrExit(deps) {
|
|
154
|
-
if (!hasGraphicalEnvironment()) {
|
|
155
|
-
deps.errorLog(new GraphicalEnvironmentNotFoundError().message);
|
|
156
|
-
deps.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Get the browser launch options from configuration, handling errors with CLI output.
|
|
161
|
-
* Exits with error if no valid browser config exists, if browser is disabled,
|
|
162
|
-
* or if no graphical environment is available.
|
|
163
|
-
*/
|
|
164
|
-
function getBrowserLaunchOptionsOrExit(deps) {
|
|
165
|
-
checkBrowserNotDisabledOrExit(deps);
|
|
166
|
-
checkGraphicalEnvironmentOrExit(deps);
|
|
167
|
-
const browserConfig = loadBrowserConfig(deps.config.configPath);
|
|
168
|
-
if (!browserConfig) {
|
|
169
|
-
deps.errorLog("Error: No browser configured. Run 'latchkey ensure-browser' first.");
|
|
170
|
-
deps.exit(1);
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
browserStatePath: deps.config.browserStatePath,
|
|
174
|
-
executablePath: browserConfig.executablePath,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
156
|
/**
|
|
178
157
|
* Register all CLI commands on the given program.
|
|
179
158
|
*/
|
|
@@ -187,56 +166,45 @@ export function registerCommands(program, deps) {
|
|
|
187
166
|
.option('--builtin', 'Only list built-in services (exclude registered services)')
|
|
188
167
|
.option('--viable', 'Only list services that either have stored credentials or can be authenticated via a browser.')
|
|
189
168
|
.action(async (options) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (options.viable === true) {
|
|
195
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
196
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
197
|
-
const allCredentials = apiCredentialStore.getAll();
|
|
198
|
-
services = services.filter((service) => {
|
|
199
|
-
if (allCredentials.has(service.name)) {
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
const supportsBrowser = service.getSession !== undefined &&
|
|
203
|
-
!deps.config.browserDisabled &&
|
|
204
|
-
hasGraphicalEnvironment();
|
|
205
|
-
return supportsBrowser;
|
|
169
|
+
if (deps.config.gatewayUrl !== null) {
|
|
170
|
+
const result = await forwardToGateway(deps, {
|
|
171
|
+
command: 'services list',
|
|
172
|
+
params: options,
|
|
206
173
|
});
|
|
174
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
175
|
+
return;
|
|
207
176
|
}
|
|
208
|
-
const
|
|
209
|
-
deps.
|
|
177
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
178
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
179
|
+
const result = servicesList(deps.registry, apiCredentialStore, deps.config, options);
|
|
180
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
210
181
|
});
|
|
211
182
|
servicesCommand
|
|
212
183
|
.command('info')
|
|
213
184
|
.description('Show information about a service.')
|
|
214
185
|
.argument('<service_name>', 'Name of the service to get info for')
|
|
215
186
|
.action(async (serviceName) => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
187
|
+
if (deps.config.gatewayUrl !== null) {
|
|
188
|
+
const info = await forwardToGateway(deps, {
|
|
189
|
+
command: 'services info',
|
|
190
|
+
params: { serviceName },
|
|
191
|
+
});
|
|
192
|
+
deps.log(JSON.stringify(info, null, 2));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
197
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
198
|
+
const info = await servicesInfo(deps.registry, apiCredentialStore, deps.config, serviceName);
|
|
199
|
+
deps.log(JSON.stringify(info, null, 2));
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof UnknownServiceError) {
|
|
203
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
204
|
+
deps.exit(1);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
221
207
|
}
|
|
222
|
-
// Login options
|
|
223
|
-
const supportsBrowser = service.getSession !== undefined && !deps.config.browserDisabled;
|
|
224
|
-
const authOptions = supportsBrowser ? ['browser', 'set'] : ['set'];
|
|
225
|
-
// Credentials status
|
|
226
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
227
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
228
|
-
const apiCredentials = apiCredentialStore.get(serviceName);
|
|
229
|
-
const credentialStatus = await getCredentialStatus(service, apiCredentials, apiCredentialStore);
|
|
230
|
-
const serviceType = service instanceof RegisteredService ? 'user-registered' : 'built-in';
|
|
231
|
-
const info = {
|
|
232
|
-
type: serviceType,
|
|
233
|
-
baseApiUrls: service.baseApiUrls,
|
|
234
|
-
authOptions,
|
|
235
|
-
credentialStatus,
|
|
236
|
-
setCredentialsExample: service.setCredentialsExample(serviceName),
|
|
237
|
-
developerNotes: service.info,
|
|
238
|
-
};
|
|
239
|
-
deps.log(JSON.stringify(info, null, 2));
|
|
240
208
|
});
|
|
241
209
|
servicesCommand
|
|
242
210
|
.command('register')
|
|
@@ -246,6 +214,7 @@ export function registerCommands(program, deps) {
|
|
|
246
214
|
.option('--service-family <name>', 'Name of the built-in service to use as a template, if any (e.g. gitlab)')
|
|
247
215
|
.option('--login-url <url>', 'Login URL for browser-based authentication, if applicable')
|
|
248
216
|
.action((rawServiceName, options) => {
|
|
217
|
+
refuseInGatewayMode(deps, 'services register');
|
|
249
218
|
let serviceName;
|
|
250
219
|
try {
|
|
251
220
|
serviceName = canonicalizeServiceName(rawServiceName);
|
|
@@ -303,6 +272,7 @@ export function registerCommands(program, deps) {
|
|
|
303
272
|
.description('Deregister a previously registered service instance.')
|
|
304
273
|
.argument('<service_name>', 'Name of the registered service to remove')
|
|
305
274
|
.action(async (serviceName) => {
|
|
275
|
+
refuseInGatewayMode(deps, 'services deregister');
|
|
306
276
|
const service = deps.registry.getByName(serviceName);
|
|
307
277
|
if (service === null) {
|
|
308
278
|
deps.errorLog(`Error: Unknown service: ${serviceName}`);
|
|
@@ -330,6 +300,7 @@ export function registerCommands(program, deps) {
|
|
|
330
300
|
.argument('[service_name]', 'Name of the service to clear API credentials for')
|
|
331
301
|
.option('-y, --yes', 'Skip confirmation prompt when clearing all data')
|
|
332
302
|
.action(async (serviceName, options) => {
|
|
303
|
+
refuseInGatewayMode(deps, 'auth clear');
|
|
333
304
|
if (serviceName === undefined) {
|
|
334
305
|
await clearAll(deps, options.yes ?? false);
|
|
335
306
|
}
|
|
@@ -341,17 +312,14 @@ export function registerCommands(program, deps) {
|
|
|
341
312
|
.command('list')
|
|
342
313
|
.description('List all stored credentials and their status.')
|
|
343
314
|
.action(async () => {
|
|
315
|
+
if (deps.config.gatewayUrl !== null) {
|
|
316
|
+
const entries = await forwardToGateway(deps, { command: 'auth list' });
|
|
317
|
+
deps.log(JSON.stringify(entries, null, 2));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
344
320
|
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
345
321
|
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
346
|
-
const
|
|
347
|
-
const statusChecks = Array.from(allCredentials, async ([serviceName, credentials]) => {
|
|
348
|
-
const service = deps.registry.getByName(serviceName);
|
|
349
|
-
const credentialStatus = service !== null
|
|
350
|
-
? await getCredentialStatus(service, credentials, apiCredentialStore)
|
|
351
|
-
: ApiCredentialStatus.Valid;
|
|
352
|
-
return [serviceName, { credentialType: credentials.objectType, credentialStatus }];
|
|
353
|
-
});
|
|
354
|
-
const entries = Object.fromEntries(await Promise.all(statusChecks));
|
|
322
|
+
const entries = await authList(deps.registry, apiCredentialStore);
|
|
355
323
|
deps.log(JSON.stringify(entries, null, 2));
|
|
356
324
|
});
|
|
357
325
|
authCommand
|
|
@@ -362,6 +330,7 @@ export function registerCommands(program, deps) {
|
|
|
362
330
|
.allowUnknownOption()
|
|
363
331
|
.allowExcessArguments()
|
|
364
332
|
.action(async (_serviceName, _options, command) => {
|
|
333
|
+
refuseInGatewayMode(deps, 'auth set');
|
|
365
334
|
const [serviceName, ...curlArguments] = command.args;
|
|
366
335
|
if (serviceName === undefined) {
|
|
367
336
|
deps.errorLog('Error: Service name is required.');
|
|
@@ -394,6 +363,7 @@ export function registerCommands(program, deps) {
|
|
|
394
363
|
.addHelpText('after', `\nExample:\n $ latchkey auth set-nocurl aws <access-key-id> <secret-access-key>`)
|
|
395
364
|
.allowExcessArguments()
|
|
396
365
|
.action(async (_serviceName, _options, command) => {
|
|
366
|
+
refuseInGatewayMode(deps, 'auth set-nocurl');
|
|
397
367
|
const [serviceName, ...noCurlArguments] = command.args;
|
|
398
368
|
if (serviceName === undefined) {
|
|
399
369
|
deps.errorLog('Error: Service name is required.');
|
|
@@ -426,33 +396,45 @@ export function registerCommands(program, deps) {
|
|
|
426
396
|
.description('Login to a service via the browser and store the API credentials.')
|
|
427
397
|
.argument('<service_name>', 'Name of the service to login to')
|
|
428
398
|
.action(async (serviceName) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (!session) {
|
|
437
|
-
deps.errorLog(`Service '${serviceName}' does not support browser flows. ` +
|
|
438
|
-
`Use '${service.setCredentialsExample(serviceName)}' to set credentials manually.`);
|
|
439
|
-
deps.exit(1);
|
|
440
|
-
}
|
|
441
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
442
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
443
|
-
const oldCredentials = apiCredentialStore.get(service.name);
|
|
444
|
-
if (session.prepare && oldCredentials === null) {
|
|
445
|
-
deps.errorLog(`Error: Service ${serviceName} requires preparation first.`);
|
|
446
|
-
deps.errorLog(`Run 'latchkey auth browser-prepare ${serviceName}' before logging in.`);
|
|
447
|
-
deps.exit(1);
|
|
399
|
+
if (deps.config.gatewayUrl !== null) {
|
|
400
|
+
await forwardToGateway(deps, {
|
|
401
|
+
command: 'auth browser',
|
|
402
|
+
params: { serviceName },
|
|
403
|
+
});
|
|
404
|
+
deps.log('Done');
|
|
405
|
+
return;
|
|
448
406
|
}
|
|
449
|
-
const launchOptions = getBrowserLaunchOptionsOrExit(deps);
|
|
450
407
|
try {
|
|
451
|
-
const
|
|
452
|
-
apiCredentialStore
|
|
408
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
409
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
410
|
+
await authBrowser(deps.registry, apiCredentialStore, encryptedStorage, deps.config, serviceName);
|
|
453
411
|
deps.log('Done');
|
|
454
412
|
}
|
|
455
413
|
catch (error) {
|
|
414
|
+
if (error instanceof UnknownServiceError) {
|
|
415
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
416
|
+
deps.exit(1);
|
|
417
|
+
}
|
|
418
|
+
if (error instanceof BrowserFlowsNotSupportedError) {
|
|
419
|
+
deps.errorLog(error.message);
|
|
420
|
+
deps.exit(1);
|
|
421
|
+
}
|
|
422
|
+
if (error instanceof PreparationRequiredError) {
|
|
423
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
424
|
+
deps.exit(1);
|
|
425
|
+
}
|
|
426
|
+
if (error instanceof BrowserDisabledError) {
|
|
427
|
+
deps.errorLog(error.message);
|
|
428
|
+
deps.exit(1);
|
|
429
|
+
}
|
|
430
|
+
if (error instanceof GraphicalEnvironmentNotFoundError) {
|
|
431
|
+
deps.errorLog(error.message);
|
|
432
|
+
deps.exit(1);
|
|
433
|
+
}
|
|
434
|
+
if (error instanceof BrowserNotConfiguredError) {
|
|
435
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
436
|
+
deps.exit(1);
|
|
437
|
+
}
|
|
456
438
|
if (error instanceof LoginCancelledError) {
|
|
457
439
|
deps.errorLog('Login cancelled.');
|
|
458
440
|
deps.exit(1);
|
|
@@ -469,32 +451,47 @@ export function registerCommands(program, deps) {
|
|
|
469
451
|
.description('Prepare a service to be used with the browser command.')
|
|
470
452
|
.argument('<service_name>', 'Name of the service to prepare')
|
|
471
453
|
.action(async (serviceName) => {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
484
|
-
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
485
|
-
// Check if already prepared (credentials exist)
|
|
486
|
-
const existingCredentials = apiCredentialStore.get(service.name);
|
|
487
|
-
if (existingCredentials !== null) {
|
|
488
|
-
deps.log('Already prepared.');
|
|
454
|
+
if (deps.config.gatewayUrl !== null) {
|
|
455
|
+
const result = (await forwardToGateway(deps, {
|
|
456
|
+
command: 'auth browser-prepare',
|
|
457
|
+
params: { serviceName },
|
|
458
|
+
}));
|
|
459
|
+
if (result !== null && result.alreadyPrepared === true) {
|
|
460
|
+
deps.log('Already prepared.');
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
deps.log('Done');
|
|
464
|
+
}
|
|
489
465
|
return;
|
|
490
466
|
}
|
|
491
|
-
const launchOptions = getBrowserLaunchOptionsOrExit(deps);
|
|
492
467
|
try {
|
|
493
|
-
const
|
|
494
|
-
apiCredentialStore
|
|
495
|
-
deps.
|
|
468
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
469
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
470
|
+
const result = await authBrowserPrepare(deps.registry, apiCredentialStore, encryptedStorage, deps.config, serviceName);
|
|
471
|
+
if (result.alreadyPrepared) {
|
|
472
|
+
deps.log('Already prepared.');
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
deps.log('Done');
|
|
476
|
+
}
|
|
496
477
|
}
|
|
497
478
|
catch (error) {
|
|
479
|
+
if (error instanceof UnknownServiceError) {
|
|
480
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
481
|
+
deps.exit(1);
|
|
482
|
+
}
|
|
483
|
+
if (error instanceof BrowserDisabledError) {
|
|
484
|
+
deps.errorLog(error.message);
|
|
485
|
+
deps.exit(1);
|
|
486
|
+
}
|
|
487
|
+
if (error instanceof GraphicalEnvironmentNotFoundError) {
|
|
488
|
+
deps.errorLog(error.message);
|
|
489
|
+
deps.exit(1);
|
|
490
|
+
}
|
|
491
|
+
if (error instanceof BrowserNotConfiguredError) {
|
|
492
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
493
|
+
deps.exit(1);
|
|
494
|
+
}
|
|
498
495
|
if (error instanceof LoginCancelledError) {
|
|
499
496
|
deps.errorLog('Preparation cancelled.');
|
|
500
497
|
deps.exit(1);
|
|
@@ -513,51 +510,106 @@ export function registerCommands(program, deps) {
|
|
|
513
510
|
.allowExcessArguments()
|
|
514
511
|
.action(async (_options, command) => {
|
|
515
512
|
const curlArguments = command.args;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
513
|
+
if (deps.config.gatewayUrl !== null) {
|
|
514
|
+
let targetUrl;
|
|
515
|
+
try {
|
|
516
|
+
targetUrl = extractUrlFromCurlArguments(curlArguments);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
if (error instanceof CurlParseError) {
|
|
520
|
+
if (error.message) {
|
|
521
|
+
deps.errorLog(`${ErrorMessages.couldNotExtractUrlBrief} ${error.message}`);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
deps.errorLog(`${ErrorMessages.couldNotExtractUrl} ${error.message}`);
|
|
525
|
+
}
|
|
526
|
+
deps.exit(1);
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
if (targetUrl === null) {
|
|
531
|
+
deps.errorLog(ErrorMessages.couldNotExtractUrl);
|
|
532
|
+
deps.exit(1);
|
|
533
|
+
}
|
|
534
|
+
let rewritten;
|
|
535
|
+
try {
|
|
536
|
+
rewritten = rewriteCurlArgumentsForGateway(curlArguments, targetUrl, deps.config.gatewayUrl);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof GatewayCurlRewriteError) {
|
|
540
|
+
deps.errorLog(`Error: ${error.message}`);
|
|
541
|
+
deps.exit(1);
|
|
542
|
+
}
|
|
543
|
+
throw error;
|
|
521
544
|
}
|
|
545
|
+
const result = deps.runCurl(rewritten);
|
|
546
|
+
deps.exit(result.returncode);
|
|
547
|
+
}
|
|
548
|
+
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
549
|
+
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
550
|
+
let finalArguments;
|
|
551
|
+
try {
|
|
552
|
+
finalArguments = await prepareCurlInvocation(curlArguments, apiCredentialStore, {
|
|
553
|
+
registry: deps.registry,
|
|
554
|
+
checkPermission: deps.checkPermission,
|
|
555
|
+
permissionsConfigPath: deps.config.permissionsConfigPath,
|
|
556
|
+
permissionsDoNotUseBuiltinSchemas: deps.config.permissionsDoNotUseBuiltinSchemas,
|
|
557
|
+
passthroughUnknown: deps.config.passthroughUnknown,
|
|
558
|
+
});
|
|
522
559
|
}
|
|
523
560
|
catch (error) {
|
|
561
|
+
if (error instanceof RequestNotPermittedError) {
|
|
562
|
+
deps.errorLog(error.message);
|
|
563
|
+
deps.exit(PERMISSION_DENIED_EXIT_CODE);
|
|
564
|
+
}
|
|
524
565
|
if (error instanceof PermissionCheckError) {
|
|
525
566
|
deps.errorLog(`Error: ${error.message}`);
|
|
526
567
|
deps.exit(PERMISSION_DENIED_EXIT_CODE);
|
|
527
568
|
}
|
|
569
|
+
if (error instanceof UrlExtractionFailedError ||
|
|
570
|
+
error instanceof NoServiceForUrlError ||
|
|
571
|
+
error instanceof NoCredentialsForServiceError ||
|
|
572
|
+
error instanceof CredentialsExpiredError) {
|
|
573
|
+
deps.errorLog(error.message);
|
|
574
|
+
deps.exit(1);
|
|
575
|
+
}
|
|
528
576
|
throw error;
|
|
529
577
|
}
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
578
|
+
const result = deps.runCurl(finalArguments);
|
|
579
|
+
deps.exit(result.returncode);
|
|
580
|
+
});
|
|
581
|
+
program
|
|
582
|
+
.command('gateway')
|
|
583
|
+
.description('Start a local HTTP gateway that proxies requests with credential injection.')
|
|
584
|
+
.option('--port <number>', `Port to listen on (default: ${deps.config.gatewayListenPort.toString()}, configurable via config.json key 'gatewayListenPort')`)
|
|
585
|
+
.option('--host <address>', `Address to bind to (default: ${deps.config.gatewayListenHost}, configurable via config.json key 'gatewayListenHost')`)
|
|
586
|
+
.option('--max-body-size <bytes>', 'Maximum request body size in bytes', String(10 * 1024 * 1024))
|
|
587
|
+
.action(async (options) => {
|
|
588
|
+
refuseInGatewayMode(deps, 'gateway');
|
|
589
|
+
const portString = options.port ?? deps.config.gatewayListenPort.toString();
|
|
590
|
+
const port = parseInt(portString, 10);
|
|
591
|
+
if (isNaN(port) || port < 0 || port > 65535) {
|
|
592
|
+
deps.errorLog(`Error: Invalid port number: ${portString}`);
|
|
533
593
|
deps.exit(1);
|
|
534
594
|
}
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
537
|
-
deps.errorLog(`Error:
|
|
595
|
+
const maxBodySize = parseInt(options.maxBodySize, 10);
|
|
596
|
+
if (isNaN(maxBodySize) || maxBodySize <= 0) {
|
|
597
|
+
deps.errorLog(`Error: Invalid max body size: ${options.maxBodySize}`);
|
|
538
598
|
deps.exit(1);
|
|
539
599
|
}
|
|
540
600
|
const encryptedStorage = await createEncryptedStorageFromConfig(deps.config);
|
|
541
601
|
const apiCredentialStore = new ApiCredentialStore(deps.config.credentialStorePath, encryptedStorage);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
deps.errorLog(`Error: Credentials for ${service.name} are expired.`);
|
|
554
|
-
deps.errorLog(`Run 'latchkey auth browser ${service.name}' or 'latchkey auth set ${service.name}' to refresh them.`);
|
|
555
|
-
deps.exit(1);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const allArguments = apiCredentials.injectIntoCurlCall(curlArguments);
|
|
559
|
-
const result = deps.runCurl(allArguments);
|
|
560
|
-
deps.exit(result.returncode);
|
|
602
|
+
const gateway = await startGateway(deps, apiCredentialStore, encryptedStorage, {
|
|
603
|
+
port,
|
|
604
|
+
host: options.host ?? deps.config.gatewayListenHost,
|
|
605
|
+
maxBodySize,
|
|
606
|
+
});
|
|
607
|
+
const shutdown = async () => {
|
|
608
|
+
await gateway.close();
|
|
609
|
+
deps.exit(0);
|
|
610
|
+
};
|
|
611
|
+
process.on('SIGINT', () => void shutdown());
|
|
612
|
+
process.on('SIGTERM', () => void shutdown());
|
|
561
613
|
});
|
|
562
614
|
program
|
|
563
615
|
.command('skill-md')
|
|
@@ -570,6 +622,7 @@ export function registerCommands(program, deps) {
|
|
|
570
622
|
.description('Ensure a Chrome/Chromium browser is available for Latchkey to use.')
|
|
571
623
|
.option('--source <sources>', `Comma-separated list of sources to try in order: ${BROWSER_SOURCES.join(', ')}`, DEFAULT_BROWSER_SOURCES.join(','))
|
|
572
624
|
.action(async (options) => {
|
|
625
|
+
refuseInGatewayMode(deps, 'ensure-browser');
|
|
573
626
|
const configPath = deps.config.configPath;
|
|
574
627
|
// Parse and validate sources
|
|
575
628
|
const sourceList = options.source.split(',').map((s) => s.trim());
|