latchkey 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +8 -8
  2. package/.nvmrc +0 -1
  3. package/.pre-commit-config.yaml +0 -22
  4. package/.prettierignore +0 -4
  5. package/.prettierrc +0 -7
  6. package/CLAUDE.md +0 -13
  7. package/docs/development.md +0 -94
  8. package/eslint.config.js +0 -30
  9. package/integrations/SKILL.md +0 -62
  10. package/scripts/cryptFile.ts +0 -123
  11. package/scripts/recordBrowserSession.ts +0 -280
  12. package/scripts/tsconfig.json +0 -10
  13. package/src/apiCredentialStore.ts +0 -87
  14. package/src/apiCredentials.ts +0 -180
  15. package/src/cli.ts +0 -32
  16. package/src/cliCommands.ts +0 -321
  17. package/src/config.ts +0 -115
  18. package/src/curl.ts +0 -78
  19. package/src/encryptedStorage.ts +0 -161
  20. package/src/encryption.ts +0 -106
  21. package/src/index.ts +0 -65
  22. package/src/keychain.ts +0 -105
  23. package/src/playwrightUtils.ts +0 -143
  24. package/src/registry.ts +0 -35
  25. package/src/services/base.ts +0 -234
  26. package/src/services/discord.ts +0 -73
  27. package/src/services/dropbox.ts +0 -173
  28. package/src/services/github.ts +0 -139
  29. package/src/services/index.ts +0 -13
  30. package/src/services/linear.ts +0 -134
  31. package/src/services/slack.ts +0 -85
  32. package/tests/apiCredentialStore.test.ts +0 -162
  33. package/tests/apiCredentials.test.ts +0 -195
  34. package/tests/cli.test.ts +0 -798
  35. package/tests/encryptedStorage.test.ts +0 -173
  36. package/tests/encryption.test.ts +0 -169
  37. package/tests/lint.test.ts +0 -19
  38. package/tests/registry.test.ts +0 -103
  39. package/tests/servicesAgainstRecordings.test.ts +0 -230
  40. package/tests/typecheck.test.ts +0 -19
  41. package/tsconfig.json +0 -24
  42. package/vitest.config.ts +0 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latchkey",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A CLI tool that injects API credentials into curl requests for known third-party services",
5
5
  "author": "Imbue <hynek@imbue.com>",
6
6
  "repository": {
@@ -12,19 +12,19 @@
12
12
  "url": "https://github.com/imbue-ai/latchkey/issues"
13
13
  },
14
14
  "type": "module",
15
- "main": "dist/index.js",
16
- "types": "dist/index.d.ts",
15
+ "main": "dist/src/index.js",
16
+ "types": "dist/src/index.d.ts",
17
17
  "bin": {
18
18
  "latchkey": "./dist/src/cli.js"
19
19
  },
20
- "scripts": {
21
- "postinstall": "npx playwright install chromium",
22
- "prepublishOnly": "npm run build",
23
- "files": [
20
+ "files": [
24
21
  "dist",
25
22
  "README.md",
26
23
  "LICENSE"
27
- ],
24
+ ],
25
+ "scripts": {
26
+ "postinstall": "npx playwright install chromium",
27
+ "prepublishOnly": "npm run build",
28
28
  "build": "tsc",
29
29
  "dev": "tsc --watch",
30
30
  "lint": "eslint src tests scripts",
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- 24.13.0
@@ -1,22 +0,0 @@
1
- default_install_hook_types: [pre-commit]
2
-
3
- repos:
4
- - repo: https://github.com/pre-commit/pre-commit-hooks
5
- rev: v6.0.0
6
- hooks:
7
- - id: trailing-whitespace
8
- - id: end-of-file-fixer
9
- - id: check-added-large-files
10
- exclude: 'uv.lock'
11
- - id: check-yaml
12
- - repo: https://github.com/astral-sh/uv-pre-commit
13
- rev: 0.7.22
14
- hooks:
15
- - id: uv-lock
16
- - repo: local
17
- hooks:
18
- - id: ruff
19
- name: "Python formatter + import sorter (ruff)"
20
- entry: bash -c 'uv run ruff check --select UP006,UP007,I,F401 --fix --force-exclude --config pyproject.toml "$@" && uv run ruff format --force-exclude --config pyproject.toml "$@"' --
21
- language: system
22
- types: [python]
package/.prettierignore DELETED
@@ -1,4 +0,0 @@
1
- dist
2
- node_modules
3
- coverage
4
- *.md
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "semi": true,
3
- "singleQuote": true,
4
- "trailingComma": "es5",
5
- "tabWidth": 2,
6
- "printWidth": 100
7
- }
package/CLAUDE.md DELETED
@@ -1,13 +0,0 @@
1
- For information about the high-level goal and motivations, see README.md.
2
-
3
- You are a highly intelligent and experienced software creator that codes in a straightforward way, prefers simplicity and avoids overengineering.
4
-
5
- Typescript style guide:
6
-
7
- - Use modern Typescript code.
8
- - Prefer functional, stateless logic as much as possible.
9
- - Use immutable data structures as much as possible.
10
- - Do not use abbreviations in variable (class, function, ...) names. It's fine for names to be somewhat verbose.
11
- - Omit docstrings if they don't add any value beyond what can be obviously inferred from the function signature / class name.
12
- - Do not throw builtin errors; always replace them with dedicated error subclasses.
13
- - When done, validate your changes by running `npm lint` and `npm test`.
@@ -1,94 +0,0 @@
1
- # Developing Latchkey
2
-
3
- Thank you for considering contributing to Latchkey!
4
-
5
- ## Setting up your environment
6
-
7
- Make sure you're using [nvm](https://github.com/nvm-sh/nvm) so that your node version
8
- corresponds to the one listed in `.nvmrc`.
9
-
10
- After that, the easiest way to set up your system so that you can run
11
- Latchkey while working on it is to clone this repository and
12
- then run:
13
-
14
- ```
15
- npm install && npm run build && npm link
16
- ```
17
-
18
- After that, every time you make a change to the code, run
19
- `npm run rebuild`. Invoking `latchkey` in your terminal will
20
- then use the version you just built.
21
-
22
- ## Before you submit a PR
23
-
24
- - Run `npm lint` and `npm test` to validate your changes.
25
- - Run `npm format` to apply autoformatting.
26
-
27
-
28
- ## Adding a new service
29
-
30
- Each third-party service needs to be approached slightly
31
- differently. When adding support for a new service, you need to
32
- start by asking yourself the following question:
33
-
34
-
35
- _Can an API token be extracted from the network traffic that flows between the browser and the service's website during or after login?_
36
-
37
- If the answer is yes, see how the [Discord](../src/services/discord.ts) service is implemented and try to do it similarly.
38
-
39
- Otherwise, ask yourself the following question:
40
-
41
- _Can an API token be created in the user's account (e.g. in Developer settings)?_
42
-
43
- If so, see how the [Linear](../src/services/linear.ts) service is implemented and try to do it similarly.
44
-
45
- When possible, the first option (extracting the token from the
46
- network traffic) is always preferable because it's simpler, more
47
- robust, and less invasive. If the answer is no in both cases,
48
- it's a special case and you're on your own!
49
-
50
- Above, when we say "API", we always mean a public API. Do
51
- not expose undocumented private APIs through Latchkey - agents
52
- should be able to determine usage by consulting the documentation.
53
-
54
-
55
- ### Potentially useful helpers
56
-
57
- #### Request / response recorder
58
-
59
- Use this to record the request/response pairs of your browser
60
- login sequence as plaintext JSON files. The resulting recording
61
- can be inspected, either manually or with the help of AI, to see
62
- if you can extract an API token or something similar from there.
63
-
64
- ```
65
- npx tsx scripts/recordBrowserSession.ts <service_name>
66
- ```
67
-
68
- If you have `jq` installed on your system, you can then
69
- start exploring, for instance like this:
70
-
71
- ```
72
- cat path/to/recording/login_session.json | jq -C | less -R
73
- ```
74
-
75
- #### File encryptor / decryptor
76
-
77
- During development, it may sometimes be necessary to inspect the
78
- credentials stored in `~/.latchkey/credentials.json.enc`.
79
-
80
- To do that, you can use `scripts/cryptFile.ts`. For example:
81
-
82
- ```
83
- npx tsx scripts/cryptFile.ts decrypt ~/.latchkey/credentials.json.enc
84
- ```
85
-
86
-
87
- #### Browser automation recorder
88
-
89
- When automating the browser login follow-up, you can sometimes
90
- use Playwright's codegen functionality, for example:
91
-
92
- ```
93
- npx playwright codegen --target=javascript https://login-page.example.com/
94
- ```
package/eslint.config.js DELETED
@@ -1,30 +0,0 @@
1
- import eslint from "@eslint/js";
2
- import tseslint from "typescript-eslint";
3
-
4
- export default tseslint.config(
5
- eslint.configs.recommended,
6
- ...tseslint.configs.strictTypeChecked,
7
- ...tseslint.configs.stylisticTypeChecked,
8
- {
9
- languageOptions: {
10
- parserOptions: {
11
- projectService: true,
12
- tsconfigRootDir: import.meta.dirname,
13
- },
14
- },
15
- },
16
- {
17
- ignores: ["dist/", "node_modules/", "eslint.config.js"],
18
- },
19
- {
20
- rules: {
21
- // Allow unused variables prefixed with underscore
22
- "@typescript-eslint/no-unused-vars": [
23
- "error",
24
- { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
25
- ],
26
- // Allow non-null assertions when needed
27
- "@typescript-eslint/no-non-null-assertion": "off",
28
- },
29
- }
30
- );
@@ -1,62 +0,0 @@
1
- ---
2
- name: latchkey
3
- description: Interact with third-party services (Slack, Discord, Dropbox, GitHub, Linear...) on user's behalf using their public APIs.
4
- ---
5
-
6
- # Latchkey
7
-
8
- ## Instructions
9
-
10
- Latchkey is a CLI tool that automatically injects credentials into curl commands for supported public APIs. Instead of manually managing API tokens, latchkey opens a browser for login, extracts credentials from the session, and injects them into your curl requests.
11
-
12
- Use this skill when the user asks you to work with third-party services like Slack, Discord, Dropbox, Github, Linear and others on their behalf.
13
-
14
- Usage:
15
-
16
- 1. **Use `latchkey curl`** instead of regular `curl` for supported services.
17
- 2. **Look for the newest documentation of the desired public API online.**
18
- 3. **Pass through all regular curl arguments** - latchkey is a transparent wrapper.
19
- 4. **Use `latchkey status <service_name>`** when you notice potentially expired credentials.
20
- 5. When the status is `invalid`, **force a new login by calling `latchkey clear <service_name>`**, then retry the curl command.
21
- 6. **Do not force a new login if the status is `valid`** - the user might just not have the necessary permissions.
22
-
23
-
24
- ## Examples
25
-
26
- ### Make an authenticated curl request
27
- ```bash
28
- latchkey curl [curl arguments]
29
- ```
30
-
31
- ### Creating a Slack channel
32
- ```bash
33
- latchkey curl -X POST 'https://slack.com/api/conversations.create' \
34
- -H 'Content-Type: application/json' \
35
- -d '{"name":"my-channel"}'
36
- ```
37
-
38
- (Notice that `-H 'Authorization: Bearer` is not present in the invocation.)
39
-
40
- ### Getting Discord user info
41
- ```bash
42
- latchkey curl 'https://discord.com/api/v10/users/@me'
43
- ```
44
-
45
- ### Clear expired credentials and force a new login to Discord
46
- ```bash
47
- latchkey status discord # Returns "invalid"
48
- latchkey clear discord
49
- latchkey curl 'https://discord.com/api/v10/users/@me'
50
- ```
51
-
52
- Only do this when you notice that your previous call ended up not being authenticated (HTTP 401 or 403). The next `latchkey curl` call will trigger a new login flow.
53
-
54
- ### List supported services
55
- ```bash
56
- latchkey services
57
- ```
58
-
59
- ## Notes
60
-
61
- - All curl arguments are passed through unchanged
62
- - Return codes, stdin, and stdout are passed back from curl
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * CLI tool for encrypting and decrypting latchkey files.
4
- *
5
- * This is a developer utility for inspecting and modifying encrypted
6
- * credential and browser state files.
7
- *
8
- * Usage:
9
- * npx tsx scripts/cryptFile.ts decrypt <file> # Decrypt file in place
10
- * npx tsx scripts/cryptFile.ts encrypt <file> # Encrypt file in place
11
- *
12
- * The encryption key is sourced from:
13
- * 1. LATCHKEY_ENCRYPTION_KEY environment variable
14
- * 2. System keychain
15
- *
16
- * Examples:
17
- * npx tsx scripts/cryptFile.ts decrypt ~/.latchkey/credentials.json
18
- * npx tsx scripts/cryptFile.ts encrypt ~/.latchkey/credentials.json
19
- */
20
-
21
- import { program } from 'commander';
22
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
23
- import { CONFIG } from '../src/config.js';
24
- import { EncryptedStorage } from '../src/encryptedStorage.js';
25
- import { encrypt, generateKey } from '../src/encryption.js';
26
- import { isKeychainAvailable, retrieveFromKeychain } from '../src/keychain.js';
27
-
28
- const ENCRYPTED_FILE_PREFIX = 'LATCHKEY_ENCRYPTED:';
29
-
30
- function getEncryptionKey(): string {
31
- // 1. Check environment variable via Config
32
- if (CONFIG.encryptionKeyOverride) {
33
- return CONFIG.encryptionKeyOverride;
34
- }
35
-
36
- // 2. Check keychain
37
- if (isKeychainAvailable(CONFIG.serviceName, CONFIG.accountName)) {
38
- const keychainKey = retrieveFromKeychain(CONFIG.serviceName, CONFIG.accountName);
39
- if (keychainKey) {
40
- return keychainKey;
41
- }
42
- }
43
-
44
- console.error(`\
45
- Error: No encryption key available.
46
- Set LATCHKEY_ENCRYPTION_KEY or ensure the system keychain has a stored key.
47
-
48
- To generate a new key:
49
- export LATCHKEY_ENCRYPTION_KEY="${generateKey()}"`);
50
- process.exit(1);
51
- }
52
-
53
- function decryptCommand(filePath: string): void {
54
- if (!existsSync(filePath)) {
55
- console.error(`Error: File not found: ${filePath}`);
56
- process.exit(1);
57
- }
58
-
59
- const rawContent = readFileSync(filePath, 'utf-8');
60
- if (!rawContent.startsWith(ENCRYPTED_FILE_PREFIX)) {
61
- console.error(`Error: File is not encrypted: ${filePath}`);
62
- process.exit(1);
63
- }
64
-
65
- const storage = new EncryptedStorage({
66
- serviceName: CONFIG.serviceName,
67
- accountName: CONFIG.accountName,
68
- });
69
- const content = storage.readFile(filePath);
70
-
71
- if (content === null) {
72
- console.error(`Error: Could not read file: ${filePath}`);
73
- process.exit(1);
74
- }
75
-
76
- writeFileSync(filePath, content, { encoding: 'utf-8', mode: 0o600 });
77
- console.error(`Decrypted: ${filePath}`);
78
- }
79
-
80
- function encryptCommand(filePath: string): void {
81
- if (!existsSync(filePath)) {
82
- console.error(`Error: File not found: ${filePath}`);
83
- process.exit(1);
84
- }
85
-
86
- const content = readFileSync(filePath, 'utf-8');
87
- if (content.startsWith(ENCRYPTED_FILE_PREFIX)) {
88
- console.error(`Error: File is already encrypted: ${filePath}`);
89
- process.exit(1);
90
- }
91
-
92
- const key = getEncryptionKey();
93
- const encryptedData = encrypt(content, key);
94
- const dataToWrite = ENCRYPTED_FILE_PREFIX + encryptedData;
95
-
96
- writeFileSync(filePath, dataToWrite, { encoding: 'utf-8', mode: 0o600 });
97
- console.error(`Encrypted: ${filePath}`);
98
- }
99
-
100
- program.name('cryptFile').description(`\
101
- CLI tool for encrypting and decrypting latchkey files.
102
-
103
- The encryption key is sourced from:
104
- 1. LATCHKEY_ENCRYPTION_KEY environment variable
105
- 2. System keychain`);
106
-
107
- program
108
- .command('decrypt')
109
- .description('Decrypt file in place')
110
- .argument('<file>', 'Path to the encrypted file')
111
- .action((filePath: string) => {
112
- decryptCommand(filePath);
113
- });
114
-
115
- program
116
- .command('encrypt')
117
- .description('Encrypt an unencrypted file in place')
118
- .argument('<file>', 'Path to the file to encrypt')
119
- .action((filePath: string) => {
120
- encryptCommand(filePath);
121
- });
122
-
123
- program.parse();
@@ -1,280 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * Record browser requests and responses during a login session.
4
- *
5
- * This script opens a browser at a service's login URL and records all HTTP
6
- * requests and responses (including their headers and timing). When you close the
7
- * browser, the recording is saved. This is useful for recording login flows that
8
- * can be replayed later for testing credentials extraction.
9
- *
10
- * Usage:
11
- * npx tsx scripts/recordBrowserSession.ts <service_name> [recording_name]
12
- *
13
- * Examples:
14
- * npx tsx scripts/recordBrowserSession.ts slack
15
- * npx tsx scripts/recordBrowserSession.ts discord custom_session.json
16
- */
17
-
18
- import { mkdirSync, writeFileSync } from 'node:fs';
19
- import { dirname, join, resolve } from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
- import type { Response } from 'playwright';
22
- import { CONFIG } from '../src/config.js';
23
- import { EncryptedStorage } from '../src/encryptedStorage.js';
24
- import { withTempBrowserContext } from '../src/playwrightUtils.js';
25
- import { REGISTRY } from '../src/registry.js';
26
-
27
- // Get the directory of this file
28
- const __filename = fileURLToPath(import.meta.url);
29
- const __dirname = dirname(__filename);
30
-
31
- // Recordings directory relative to this script
32
- const RECORDINGS_DIRECTORY = resolve(__dirname, 'recordings');
33
-
34
- // Default recording filename
35
- const DEFAULT_RECORDING_NAME = 'login_session.json';
36
-
37
- class UnknownServiceError extends Error {
38
- constructor(serviceName: string) {
39
- super(`Unknown service: ${serviceName}`);
40
- this.name = 'UnknownServiceError';
41
- }
42
- }
43
-
44
- // Resource types to skip (CSS, images, fonts, multimedia)
45
- const SKIPPED_RESOURCE_TYPES = new Set(['stylesheet', 'image', 'media', 'font']);
46
-
47
- // Common multi-part TLDs
48
- const MULTI_PART_TLDS = new Set(['co.uk', 'com.au', 'co.nz', 'co.jp', 'com.br', 'co.in']);
49
-
50
- interface RequestData {
51
- timestamp_ms: number;
52
- method: string;
53
- url: string;
54
- headers: Record<string, string>;
55
- resource_type: string;
56
- post_data?: string;
57
- }
58
-
59
- interface ResponseData {
60
- status: number;
61
- status_text: string;
62
- headers: Record<string, string>;
63
- body?: string;
64
- }
65
-
66
- interface RecordedEntry {
67
- request: RequestData;
68
- response: ResponseData;
69
- }
70
-
71
- /**
72
- * Extract the base domain from a URL.
73
- *
74
- * For example:
75
- * https://discord.com/login -> discord.com
76
- * https://api.discord.com/v9/users -> discord.com
77
- * https://www.example.co.uk/page -> example.co.uk
78
- */
79
- function extractBaseDomain(url: string): string {
80
- let hostname: string;
81
- try {
82
- hostname = new URL(url).hostname;
83
- } catch {
84
- return '';
85
- }
86
-
87
- // Split the hostname into parts
88
- const parts = hostname.split('.');
89
-
90
- // Handle common multi-part TLDs (e.g., co.uk, com.au)
91
- if (parts.length >= 3) {
92
- const potentialTld = parts.slice(-2).join('.');
93
- if (MULTI_PART_TLDS.has(potentialTld)) {
94
- return parts.slice(-3).join('.');
95
- }
96
- }
97
-
98
- if (parts.length >= 2) {
99
- return parts.slice(-2).join('.');
100
- }
101
-
102
- return hostname;
103
- }
104
-
105
- /**
106
- * Check if a request URL belongs to the same base domain.
107
- */
108
- function isSameBaseDomain(requestUrl: string, baseDomain: string): boolean {
109
- let requestHostname: string;
110
- try {
111
- requestHostname = new URL(requestUrl).hostname;
112
- } catch {
113
- return false;
114
- }
115
- return requestHostname === baseDomain || requestHostname.endsWith('.' + baseDomain);
116
- }
117
-
118
- /**
119
- * Handle a response and record both request and response details.
120
- */
121
- async function handleResponse(
122
- response: Response,
123
- recordedEntries: RecordedEntry[],
124
- startTime: { value: number },
125
- baseDomain: string
126
- ): Promise<void> {
127
- const request = response.request();
128
-
129
- // Skip CSS, images, fonts, and multimedia
130
- if (SKIPPED_RESOURCE_TYPES.has(request.resourceType())) {
131
- return;
132
- }
133
-
134
- // Skip requests to external domains
135
- if (!isSameBaseDomain(request.url(), baseDomain)) {
136
- return;
137
- }
138
-
139
- if (startTime.value === 0) {
140
- startTime.value = Date.now();
141
- }
142
-
143
- const timestampMs = Date.now() - startTime.value;
144
-
145
- const requestData: RequestData = {
146
- timestamp_ms: timestampMs,
147
- method: request.method(),
148
- url: request.url(),
149
- headers: await request.allHeaders(),
150
- resource_type: request.resourceType(),
151
- };
152
-
153
- // Include POST data if present
154
- try {
155
- const postData = request.postData();
156
- if (postData !== null) {
157
- requestData.post_data = postData;
158
- }
159
- } catch {
160
- // Post data not available or not decodable
161
- }
162
-
163
- const responseData: ResponseData = {
164
- status: response.status(),
165
- status_text: response.statusText(),
166
- headers: await response.allHeaders(),
167
- };
168
-
169
- // Try to get response body as text (skip binary content)
170
- try {
171
- const body = await response.text();
172
- responseData.body = body;
173
- } catch {
174
- // Binary content or other error - skip body
175
- }
176
-
177
- recordedEntries.push({
178
- request: requestData,
179
- response: responseData,
180
- });
181
- }
182
-
183
- /**
184
- * Record browser requests and responses during a login session.
185
- */
186
- async function record(
187
- serviceName: string,
188
- recordingName: string = DEFAULT_RECORDING_NAME
189
- ): Promise<void> {
190
- const service = REGISTRY.getByName(serviceName);
191
- if (service === null) {
192
- throw new UnknownServiceError(serviceName);
193
- }
194
-
195
- const outputDirectory = join(RECORDINGS_DIRECTORY, serviceName);
196
- mkdirSync(outputDirectory, { recursive: true });
197
- const requestsPath = join(outputDirectory, recordingName);
198
-
199
- const browserStatePath = CONFIG.browserStatePath;
200
-
201
- const baseDomain = extractBaseDomain(service.loginUrl);
202
-
203
- console.log(`Recording login for service: ${service.name}`);
204
- console.log(`Login URL: ${service.loginUrl}`);
205
- console.log(`Recording requests to: ${baseDomain} (and subdomains)`);
206
- console.log(`Output directory: ${outputDirectory}`);
207
- console.log(`Browser state: ${browserStatePath}`);
208
- console.log("\nClose the browser window when you're done to save the recording.");
209
-
210
- const recordedEntries: RecordedEntry[] = [];
211
- const startTime = { value: 0 };
212
-
213
- const encryptedStorage = new EncryptedStorage({
214
- encryptionKeyOverride: CONFIG.encryptionKeyOverride,
215
- serviceName: CONFIG.serviceName,
216
- accountName: CONFIG.accountName,
217
- });
218
-
219
- await withTempBrowserContext(encryptedStorage, browserStatePath, async ({ context }) => {
220
- const page = await context.newPage();
221
-
222
- // Register response handler to capture all requests and responses
223
- page.on('response', (response) => {
224
- handleResponse(response, recordedEntries, startTime, baseDomain).catch(() => {
225
- // Ignore errors in response handling
226
- });
227
- });
228
-
229
- await page.goto(service.loginUrl);
230
-
231
- // Wait for user to close the browser
232
- await page.waitForEvent('close', { timeout: 0 });
233
- });
234
-
235
- // Save recorded entries
236
- writeFileSync(requestsPath, JSON.stringify(recordedEntries, null, 2));
237
-
238
- console.log('\nRecording saved successfully!');
239
- console.log(` Requests file: ${requestsPath}`);
240
- console.log(` Recorded ${String(recordedEntries.length)} request/response pairs`);
241
- }
242
-
243
- // Main entry point
244
- async function main(): Promise<void> {
245
- const args = process.argv.slice(2);
246
-
247
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
248
- console.log('Usage: npx tsx scripts/recordBrowserSession.ts <service_name> [recording_name]');
249
- console.log('');
250
- console.log('Arguments:');
251
- console.log(
252
- " service_name Name of the service to record login for (e.g., 'slack', 'discord')"
253
- );
254
- console.log(' recording_name Name of the recording file (default: login_session.json)');
255
- console.log('');
256
- console.log('Examples:');
257
- console.log(' npx tsx scripts/recordBrowserSession.ts slack');
258
- console.log(' npx tsx scripts/recordBrowserSession.ts discord custom_session.json');
259
- process.exit(0);
260
- }
261
-
262
- const serviceName = args[0]!;
263
- const recordingName = args[1] ?? DEFAULT_RECORDING_NAME;
264
-
265
- try {
266
- await record(serviceName, recordingName);
267
- } catch (error) {
268
- if (error instanceof UnknownServiceError) {
269
- console.error(`Error: ${error.message}`);
270
- console.error('Available services:');
271
- for (const service of REGISTRY.services) {
272
- console.error(` - ${service.name}`);
273
- }
274
- process.exit(1);
275
- }
276
- throw error;
277
- }
278
- }
279
-
280
- void main();
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "..",
5
- "outDir": "../dist/scripts",
6
- "noEmit": true
7
- },
8
- "include": ["./**/*", "../src/**/*"],
9
- "exclude": ["../node_modules", "../dist"]
10
- }