Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ latchkey curl -X POST 'https://slack.com/api/conversations.create' \
Latchkey is a command-line tool that injects credentials to curl requests to known public APIs.

- `latchkey services`
- Get a list of known and supported third-party services (Slack, Discord, Linear, GitHub, etc.).
- Get a list of known and supported third-party services (Slack, Discord, Linear, GitHub, Dropbox, Databricks).
- `latchkey curl <arguments>`
- Automatically inject credentials to your otherwise standard curl calls to public APIs.
- (The first time you access a service, a browser pop-up with a login screen appears.)
Expand All @@ -40,15 +40,17 @@ for details.
### Prerequisites

- `curl` and `npm` need to be present on your system.
- Node.js >= 22.9.0 and npm >= 11.6.0 (upgrade with `npm install -g npm@latest`)
- The browser requires a graphical environment.


### Steps

1. Clone this repository to your local machine.
2. Enter the repository's directory.
3. `npm install -g .`

**nvm users**: Global packages are per node version. If you switch versions, reinstall with `npm install -g .`

## Integrations

Warning: giving AI agents access to your API credentials is potentially
Expand Down Expand Up @@ -143,6 +145,12 @@ state file), run:
latchkey clear
```

### Databricks

```
latchkey curl 'https://dbc-b28fe787-b68d.cloud.databricks.com/ajax-api/2.0/mlflow/experiments/list'
```

### Advanced configuration

You can set these environment variables to override certain
Expand Down
7 changes: 0 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"latchkey": "./dist/src/cli.js"
},
"scripts": {
"prepare": "npm run build",
"postinstall": "npx playwright install chromium",
"build": "tsc",
"dev": "tsc --watch",
Expand Down
49 changes: 49 additions & 0 deletions src/apiCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,57 @@ export class SlackApiCredentials implements ApiCredentials {
}
}

/**
* Databricks-specific credentials (all cookies + CSRF token).
*/
export const DatabricksApiCredentialsSchema = z.object({
objectType: z.literal('databricks'),
cookies: z.string(), // All cookies as a single string for curl -b
csrfToken: z.string(),
workspaceUrl: z.string(),
});

export type DatabricksApiCredentialsData = z.infer<typeof DatabricksApiCredentialsSchema>;

export class DatabricksApiCredentials implements ApiCredentials {
readonly objectType = 'databricks' as const;

constructor(
readonly cookies: string,
readonly csrfToken: string,
readonly workspaceUrl: string
) {}

asCurlArguments(): readonly string[] {
const args: string[] = ['-b', this.cookies];
if (this.csrfToken) {
args.push('-H', `x-csrf-token: ${this.csrfToken}`);
}
return args;
}

toJSON(): DatabricksApiCredentialsData {
return {
objectType: this.objectType,
cookies: this.cookies,
csrfToken: this.csrfToken,
workspaceUrl: this.workspaceUrl,
};
}

static fromJSON(data: DatabricksApiCredentialsData): DatabricksApiCredentials {
return new DatabricksApiCredentials(data.cookies, data.csrfToken, data.workspaceUrl);
}
}

/**
* Union schema for all credential types.
*/
export const ApiCredentialsSchema = z.discriminatedUnion('objectType', [
AuthorizationBearerSchema,
AuthorizationBareSchema,
SlackApiCredentialsSchema,
DatabricksApiCredentialsSchema,
]);

export type ApiCredentialsData = z.infer<typeof ApiCredentialsSchema>;
Expand All @@ -147,6 +191,8 @@ export function deserializeCredentials(data: ApiCredentialsData): ApiCredentials
return AuthorizationBare.fromJSON(data);
case 'slack':
return SlackApiCredentials.fromJSON(data);
case 'databricks':
return DatabricksApiCredentials.fromJSON(data);
default: {
const exhaustiveCheck: never = data;
throw new ApiCredentialsSerializationError(
Expand All @@ -169,6 +215,9 @@ export function serializeCredentials(credentials: ApiCredentials): ApiCredential
if (credentials instanceof SlackApiCredentials) {
return credentials.toJSON();
}
if (credentials instanceof DatabricksApiCredentials) {
return credentials.toJSON();
}
throw new ApiCredentialsSerializationError(`Unknown credential type: ${credentials.objectType}`);
}

Expand Down
6 changes: 4 additions & 2 deletions src/cliCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,10 @@ export function registerCommands(program: Command, deps: CliDependencies): void
.command('services')
.description('List known and supported third-party services.')
.action(() => {
const serviceNames = deps.registry.services.map((service) => service.name);
deps.log(serviceNames.join(' '));
const staticServices = deps.registry.services.map((service) => service.name);
// Add dynamic services that aren't in the static registry
const allServices = [...staticServices, 'databricks'];
deps.log(allServices.join(' '));
});

program
Expand Down
18 changes: 17 additions & 1 deletion src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
* Service registry for looking up services by name or URL.
*/

import { Service, SLACK, DISCORD, DROPBOX, GITHUB, LINEAR } from './services/index.js';
import {
Service,
SLACK,
DISCORD,
DROPBOX,
GITHUB,
LINEAR,
isDatabricksUrl,
createDatabricksService,
} from './services/index.js';

export class Registry {
readonly services: readonly Service[];
Expand All @@ -21,13 +30,20 @@ export class Registry {
}

getByUrl(url: string): Service | null {
// Check static services first
for (const service of this.services) {
for (const baseApiUrl of service.baseApiUrls) {
if (url.startsWith(baseApiUrl)) {
return service;
}
}
}

// Check dynamic services (Databricks)
if (isDatabricksUrl(url)) {
return createDatabricksService(url);
}

return null;
}
}
Expand Down
Loading