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.
Files changed (99) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +93 -56
  3. package/dist/integrations/SKILL.md +77 -0
  4. package/dist/package.json +67 -0
  5. package/dist/scripts/encryptFile.d.ts +21 -0
  6. package/dist/scripts/encryptFile.d.ts.map +1 -0
  7. package/dist/scripts/encryptFile.js +101 -0
  8. package/dist/scripts/encryptFile.js.map +1 -0
  9. package/dist/src/apiCredentialStore.d.ts +1 -0
  10. package/dist/src/apiCredentialStore.d.ts.map +1 -1
  11. package/dist/src/apiCredentialStore.js +12 -0
  12. package/dist/src/apiCredentialStore.js.map +1 -1
  13. package/dist/src/apiCredentials.d.ts +116 -1
  14. package/dist/src/apiCredentials.d.ts.map +1 -1
  15. package/dist/src/apiCredentials.js +119 -1
  16. package/dist/src/apiCredentials.js.map +1 -1
  17. package/dist/src/browserState.d.ts +8 -0
  18. package/dist/src/browserState.d.ts.map +1 -0
  19. package/dist/src/browserState.js +21 -0
  20. package/dist/src/browserState.js.map +1 -0
  21. package/dist/src/cli.js +5 -3
  22. package/dist/src/cli.js.map +1 -1
  23. package/dist/src/cliCommands.d.ts.map +1 -1
  24. package/dist/src/cliCommands.js +218 -81
  25. package/dist/src/cliCommands.js.map +1 -1
  26. package/dist/src/config.d.ts +13 -0
  27. package/dist/src/config.d.ts.map +1 -1
  28. package/dist/src/config.js +50 -4
  29. package/dist/src/config.js.map +1 -1
  30. package/dist/src/index.d.ts +1 -1
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/index.js +2 -2
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/oauthUtils.d.ts +49 -0
  35. package/dist/src/oauthUtils.d.ts.map +1 -0
  36. package/dist/src/oauthUtils.js +183 -0
  37. package/dist/src/oauthUtils.js.map +1 -0
  38. package/dist/src/playwrightUtils.d.ts +14 -1
  39. package/dist/src/playwrightUtils.d.ts.map +1 -1
  40. package/dist/src/playwrightUtils.js +37 -8
  41. package/dist/src/playwrightUtils.js.map +1 -1
  42. package/dist/src/registry.d.ts.map +1 -1
  43. package/dist/src/registry.js +20 -4
  44. package/dist/src/registry.js.map +1 -1
  45. package/dist/src/services/base.d.ts +43 -15
  46. package/dist/src/services/base.d.ts.map +1 -1
  47. package/dist/src/services/base.js +49 -9
  48. package/dist/src/services/base.js.map +1 -1
  49. package/dist/src/services/discord.d.ts +4 -3
  50. package/dist/src/services/discord.d.ts.map +1 -1
  51. package/dist/src/services/discord.js +6 -22
  52. package/dist/src/services/discord.js.map +1 -1
  53. package/dist/src/services/dropbox.d.ts +5 -4
  54. package/dist/src/services/dropbox.d.ts.map +1 -1
  55. package/dist/src/services/dropbox.js +10 -27
  56. package/dist/src/services/dropbox.js.map +1 -1
  57. package/dist/src/services/github.d.ts +5 -4
  58. package/dist/src/services/github.d.ts.map +1 -1
  59. package/dist/src/services/github.js +21 -30
  60. package/dist/src/services/github.js.map +1 -1
  61. package/dist/src/services/google.d.ts +34 -0
  62. package/dist/src/services/google.d.ts.map +1 -0
  63. package/dist/src/services/google.js +336 -0
  64. package/dist/src/services/google.js.map +1 -0
  65. package/dist/src/services/index.d.ts +4 -2
  66. package/dist/src/services/index.d.ts.map +1 -1
  67. package/dist/src/services/index.js +4 -1
  68. package/dist/src/services/index.js.map +1 -1
  69. package/dist/src/services/linear.d.ts +5 -4
  70. package/dist/src/services/linear.d.ts.map +1 -1
  71. package/dist/src/services/linear.js +10 -29
  72. package/dist/src/services/linear.js.map +1 -1
  73. package/dist/src/services/mailchimp.d.ts +11 -0
  74. package/dist/src/services/mailchimp.d.ts.map +1 -0
  75. package/dist/src/services/mailchimp.js +16 -0
  76. package/dist/src/services/mailchimp.js.map +1 -0
  77. package/dist/src/services/notion.d.ts +29 -0
  78. package/dist/src/services/notion.d.ts.map +1 -0
  79. package/dist/src/services/notion.js +102 -0
  80. package/dist/src/services/notion.js.map +1 -0
  81. package/dist/src/services/slack.d.ts +3 -1
  82. package/dist/src/services/slack.d.ts.map +1 -1
  83. package/dist/src/services/slack.js +5 -5
  84. package/dist/src/services/slack.js.map +1 -1
  85. package/dist/src/skillMd.d.ts +2 -0
  86. package/dist/src/skillMd.d.ts.map +1 -0
  87. package/dist/src/skillMd.js +19 -0
  88. package/dist/src/skillMd.js.map +1 -0
  89. package/dist/tests/apiCredentials.test.js +59 -1
  90. package/dist/tests/apiCredentials.test.js.map +1 -1
  91. package/dist/tests/cli.test.js +283 -104
  92. package/dist/tests/cli.test.js.map +1 -1
  93. package/dist/tests/playwrightDownload.test.js +2 -2
  94. package/dist/tests/playwrightDownload.test.js.map +1 -1
  95. package/dist/tests/registry.test.js +28 -3
  96. package/dist/tests/registry.test.js.map +1 -1
  97. package/dist/tests/servicesAgainstRecordings.test.js +3 -0
  98. package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
  99. package/package.json +6 -6
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2026 Imbue (Generally Intelligent, Inc.)
1
+ Copyright 2026 Imbue, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
4
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Latchkey
2
2
 
3
- Turn browser logins into usable credentials for local agents.
3
+ Inject API credentials into local agent requests.
4
4
 
5
5
  ## Quick example
6
6
 
@@ -12,75 +12,88 @@ latchkey curl -X POST 'https://slack.com/api/conversations.create' \
12
12
 
13
13
  ## Overview
14
14
 
15
- Latchkey is a command-line tool that injects credentials to curl requests to known public APIs.
15
+ Latchkey is a command-line tool that injects credentials into curl
16
+ requests to known public APIs.
16
17
 
17
- - `latchkey services`
18
- - Get a list of known and supported third-party services (Slack, Discord, Linear, GitHub, etc.).
18
+ - `latchkey services list`
19
+ - Get a list of supported third-party services (Slack, Google Workspace, Linear, GitHub, etc.).
19
20
  - `latchkey curl <arguments>`
20
21
  - Automatically inject credentials to your otherwise standard curl calls to public APIs.
21
- - (The first time you access a service, a browser pop-up with a login screen appears.)
22
-
23
- Latchkey is primarily designed for AI agents. By invoking Latchkey, agents can
24
- prompt the user to authenticate when needed, then continue interacting with
25
- third-party APIs using standard curl syntax - no custom integrations or embedded
26
- credentials required.
27
-
28
- Unlike OAuth-based flows or typical MCP-style integrations, Latchkey does not
29
- introduce an intermediary between the agent and the service. Requests are made
30
- directly on the user’s behalf, which enables greater flexibility at the cost of
31
- formal delegation: agents authenticate as the user.
32
-
33
- If a service you need isn’t supported yet, contributions are welcome. Adding
34
- support typically involves writing a small browser automation class that
35
- extracts API credentials after login. See the [development docs](docs/development.md)
36
- for details.
22
+ - Credentials must already exist (see below).
23
+ - `latchkey auth set <service_name> <curl_arguments>`
24
+ - Manually store credentials for a service in the form of arbitrary curl arguments.
25
+ - `latchkey auth browser <service_name>`
26
+ - Open a browser login pop-up window and store the resulting API credentials.
27
+ - Only some services support this option.
28
+
29
+ Latchkey is primarily designed for AI agents. By invoking
30
+ Latchkey, agents can prompt the user to authenticate when needed,
31
+ then continue interacting with third-party APIs using standard
32
+ curl syntax - no custom integrations or embedded credentials
33
+ required.
34
+
35
+ Unlike OAuth-based flows or typical MCP-style integrations,
36
+ Latchkey does not introduce an intermediary between the agent
37
+ and the service. When `browser` is used, requests are made
38
+ directly on the user’s behalf, which enables greater flexibility
39
+ at the cost of formal delegation: agents authenticate as the
40
+ user.
41
+
42
+ If a service you need isn’t supported yet, contributions are welcome!
43
+ See the [development docs](docs/development.md) for details.
37
44
 
38
45
  ## Installation
39
46
 
40
47
  ### Prerequisites
41
48
 
42
49
  - `curl`, `node` and `npm` need to be present on your system in reasonably recent versions.
43
- - The browser requires a graphical environment.
50
+ - The `latchkey auth browser` subcommand requires a graphical environment.
44
51
 
45
52
  ### Steps
46
53
 
47
54
  ```
48
55
  npm install -g latchkey
56
+
57
+ # Optionally, if you intend to use `latchkey auth browser`:
49
58
  latchkey ensure-browser
50
59
  ```
51
60
 
52
- The `ensure-browser` command discovers and configures a browser for Latchkey to use. It searches for Chrome, Chromium, or Edge on your system. If none is found, it downloads Chromium via Playwright.
53
-
54
- **nvm users**: Global packages are per node version. If you switch versions, reinstall with `npm install -g latchkey`
61
+ The `ensure-browser` command discovers and configures a browser
62
+ for Latchkey to use. It searches for Chrome, Chromium, or Edge
63
+ on your system. If none is found, it downloads Chromium via
64
+ Playwright.
55
65
 
56
66
  ## Integrations
57
67
 
58
- Warning: giving AI agents access to your API credentials is potentially
59
- dangerous. They will be able to perform most of the actions you can. Only do this if
60
- you're willing to accept the risks.
68
+ Warning: giving AI agents access to your API credentials is
69
+ potentially dangerous, especially when using the `auth browser`
70
+ feature. They will be able to perform most of the actions you
71
+ can. Only do this if you're willing to accept the risks.
61
72
 
62
73
 
63
74
  ### OpenCode
64
75
  ```
65
76
  mkdir -p ~/.opencode/skills/latchkey
66
- cp integrations/SKILL.md ~/.opencode/skills/latchkey/SKILL.md
77
+ latchkey skill-md > ~/.opencode/skills/latchkey/SKILL.md
67
78
  ```
68
79
 
69
80
  ### Claude Code
70
81
  ```
71
82
  mkdir -p ~/.claude/skills/latchkey
72
- cp integrations/SKILL.md ~/.claude/skills/latchkey/SKILL.md
83
+ latchkey skill-md > ~/.claude/skills/latchkey/SKILL.md
73
84
  ```
74
85
 
75
86
  ### Codex
76
87
  ```
77
88
  mkdir -p ~/.codex/skills/latchkey
78
- cp integrations/SKILL.md ~/.codex/skills/latchkey/SKILL.md
89
+ latchkey skill-md > ~/.codex/skills/latchkey/SKILL.md
79
90
  ```
80
91
 
81
92
  ### Passepartout
82
93
 
83
- Check out our [Passepartout demo app](https://github.com/imbue-ai/passepartout) for an idea of how to build AI assistants for non-technical users on top of Latchkey.
94
+ Check out our [Passepartout demo app](https://github.com/imbue-ai/passepartout)
95
+ for an idea of how to build AI assistants for non-technical
96
+ users on top of Latchkey.
84
97
 
85
98
 
86
99
  ## Demo
@@ -98,17 +111,28 @@ latchkey curl -X POST 'https://slack.com/api/conversations.create' \
98
111
  -d '{"name":"something-urgent"}'
99
112
  ```
100
113
 
101
- Notice that `-H 'Authorization: Bearer ...'` is absent. This is because Latchkey:
114
+ Notice that `-H 'Authorization: Bearer ...'` is absent. This is
115
+ because Latchkey injects stored credentials automatically. To
116
+ set up credentials for a service (Slack in this example), run:
102
117
 
103
- - Opens the browser with a login screen.
104
- - After the user logs in, Latchkey extracts the necessary API credentials from the browser session.
105
- - The browser is closed, the credentials are injected into the arguments, and `curl` is invoked.
106
- - The credentials are stored so that they can be reused the next time.
118
+ ```
119
+ latchkey auth browser slack
120
+ ```
121
+
122
+ This opens the browser with a login screen. After you log in, Latchkey extracts
123
+ the necessary API credentials from the browser session, closes the browser, and
124
+ stores the credentials so that they can be reused.
125
+
126
+ Alternatively, you can provide credentials manually:
127
+
128
+ ```
129
+ latchkey auth set slack -H "Authorization: Bearer xoxb-your-token"
130
+ ```
107
131
 
108
- Otherwise, `latchkey curl` passes your arguments straight
109
- through to `curl` so you can use the same interface you are used
110
- to. The return code, stdin and stdout are passed back from curl
111
- to the caller of `latchkey`.
132
+ `latchkey curl` passes your arguments straight through to `curl`
133
+ so you can use the same interface you are used to. The return
134
+ code, stdout and stderr are passed back from curl to the caller
135
+ of `latchkey`.
112
136
 
113
137
  ### Remembering API credentials
114
138
 
@@ -120,9 +144,9 @@ encrypted.
120
144
 
121
145
  ### Inspecting the status of stored credentials
122
146
 
123
- Calling `latchkey status <service_name>` will give you
124
- information about the status of remembered credentials for the
125
- given service. Possible results are:
147
+ Calling `latchkey services info <service_name>` will show information
148
+ about the service, including the credentials status. The
149
+ credentials status line will show one of:
126
150
 
127
151
  - `missing`
128
152
  - `invalid`
@@ -132,30 +156,42 @@ given service. Possible results are:
132
156
 
133
157
  Remembered API credentials can expire. The caller of `latchkey
134
158
  curl` will typically notice this because the calls will start returning
135
- HTTP 401 or 403. To verify that, first call `latchkey status`, e.g.:
159
+ HTTP 401 or 403. To verify that, first call `latchkey services info`, e.g.:
160
+
161
+ ```
162
+ latchkey services info discord
163
+ ```
164
+
165
+ If the credentials status is `invalid`, it means the Unauthorized/Forbidden
166
+ responses are caused by invalid or expired credentials rather than insufficient
167
+ permissions. In that case, log in again:
136
168
 
137
169
  ```
138
- latchkey status discord
170
+ latchkey auth browser discord
139
171
  ```
140
172
 
141
- If the result is `invalid` , meaning the Unauthorized/Forbidden responses are
142
- caused by invalid or expired credentials rather than insufficient permissions,
143
- force a new login in the next `latchkey curl` call by clearing the remembered
144
- API credentials for the service in question, e.g.:
173
+ Or alternatively:
145
174
 
146
175
  ```
147
- latchkey clear discord
176
+ latchkey auth set discord -H "Authorization: ..."
148
177
  ```
149
178
 
150
- The next `latchkey curl` call will then trigger a new login flow.
151
179
 
152
- To clear all stored data (both the credentials store and browser
153
- state file), run:
180
+ ### Clearing credentials and logins
181
+
182
+ In case you want to remove stored API credentials, use the `auth clear` subcommand.
154
183
 
155
184
  ```
156
- latchkey clear
185
+ latchkey auth clear discord
157
186
  ```
158
187
 
188
+ To clear all stored data (both the credential store and browser state file), run:
189
+
190
+ ```
191
+ latchkey auth clear
192
+ ```
193
+
194
+
159
195
  ### Advanced configuration
160
196
 
161
197
  You can set these environment variables to override certain
@@ -166,17 +202,18 @@ containing stored API credentials
166
202
  - `LATCHKEY_BROWSER_STATE`: path to the (typically encrypted) file
167
203
  containing the state (cookies, local storage, etc.) of
168
204
  the browser used for the login popup
169
- - `LATCHKEY_CURL_PATH`: path to the curl binary
205
+ - `LATCHKEY_CURL`: path to the curl binary
170
206
  - `LATCHKEY_CONFIG`: path to the configuration file
171
207
  (defaults to `~/.latchkey/config.json`)
172
208
  - `LATCHKEY_KEYRING_SERVICE_NAME`, `LATCHKEY_KEYRING_ACCOUNT_NAME`: identifiers that are used to store the encryption password in your keyring
209
+ - `LATCHKEY_DISABLE_BROWSER`: when set (to any non-empty value), disables the browser login flow; commands that would trigger a browser login (`auth browser`, `auth browser-prepare`) will fail with an error instead
173
210
 
174
211
 
175
212
  ## Disclaimers
176
213
 
177
214
  - This is still a work in progress.
178
215
  - Latchkey has been created with the help of AI-assisted coding tools with careful human curation.
179
- - Invoking `latchkey curl ...` can sometimes have side effects in the form of
216
+ - Invoking `latchkey auth browser ...` can sometimes have side effects in the form of
180
217
  new API keys being created in your accounts (through browser automation).
181
218
  - Using agents for automated access may be prohibited by some services' ToS.
182
219
  - We reserve the right to change the license of future releases of Latchkey.
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: latchkey
3
+ description: Interact with third-party services (Slack, Google Workspace, Dropbox, GitHub, Linear...) on the user's behalf using their public APIs.
4
+ compatibility: Requires node.js, curl and latchkey (npm install -g latchkey). A desktop/GUI environment is required for the browser functionality.
5
+ ---
6
+
7
+ # Latchkey
8
+
9
+ ## Instructions
10
+
11
+ Latchkey is a CLI tool that automatically injects credentials into curl commands for supported public APIs. Credentials (mostly API tokens) can be either manually managed or, for some services, Latchkey can open a browser login pop-up window and extract API credentials from the session.
12
+
13
+ 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.
14
+
15
+ Usage:
16
+
17
+ 1. **Use `latchkey curl`** instead of regular `curl` for supported services.
18
+ 2. **Pass through all regular curl arguments** - latchkey is a transparent wrapper.
19
+ 3. **Use `latchkey services list`** to get a list of supported services.
20
+ 4. **Use `latchkey services info <service_name>`** to get information about a specific service (auth options, credentials status, API docs links, special requirements, etc.).
21
+ 5. **If necessary, get or renew credentials first.** Run `latchkey auth browser <service_name>` to open a browser login pop-up window if supported.
22
+ 6. **Look for the newest documentation of the desired public API online.** If using the `browser` auth command, avoid bot-only endpoints.
23
+ 7. **Do not initiate a new login if the credentials status is `valid`** - the user might just not have the necessary permissions for the action you're trying to do.
24
+
25
+
26
+ ## Examples
27
+
28
+ ### Make an authenticated curl request
29
+ ```bash
30
+ latchkey curl [curl arguments]
31
+ ```
32
+
33
+ ### Creating a Slack channel
34
+ ```bash
35
+ latchkey curl -X POST 'https://slack.com/api/conversations.create' \
36
+ -H 'Content-Type: application/json' \
37
+ -d '{"name":"my-channel"}'
38
+ ```
39
+
40
+ (Notice that `-H 'Authorization: Bearer` is not present in the invocation.)
41
+
42
+ ### Getting Discord user info
43
+ ```bash
44
+ latchkey curl 'https://discord.com/api/v10/users/@me'
45
+ ```
46
+
47
+ ### Detect expired credentials and force a new login to Discord
48
+ ```bash
49
+ latchkey services info discord # Check the "credentialStatus" field - shows "invalid"
50
+ latchkey auth browser discord
51
+ latchkey curl 'https://discord.com/api/v10/users/@me'
52
+ ```
53
+
54
+ Only do this when you notice that your previous call ended up not being authenticated (HTTP 401 or 403).
55
+
56
+ ### List available services
57
+ ```bash
58
+ latchkey services list
59
+ ```
60
+
61
+ Lists all services that latchkey knows about.
62
+
63
+ ### Get service-specific info
64
+ ```bash
65
+ latchkey services info slack
66
+ ```
67
+
68
+ Returns auth options, credentials status, and developer notes
69
+ about the service. If `browser` is not present in the
70
+ `authOptions` field, the service requires the user to directly
71
+ set API credentials via `latchkey auth set` before making
72
+ requests.
73
+
74
+ ## Notes
75
+
76
+ - All curl arguments are passed through unchanged
77
+ - Return code, stdout and stderr are passed back from curl
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "latchkey",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool that injects API credentials into curl requests for known third-party services",
5
+ "author": "Imbue <hynek@imbue.com>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/imbue-ai/latchkey.git"
9
+ },
10
+ "homepage": "https://github.com/imbue-ai/latchkey#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/imbue-ai/latchkey/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "dist/src/index.js",
16
+ "types": "dist/src/index.d.ts",
17
+ "bin": {
18
+ "latchkey": "./dist/src/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "prepublishOnly": "npm run build",
27
+ "build": "tsc && node -e \"require('fs').cpSync('integrations', 'dist/integrations', {recursive:true})\" ",
28
+ "dev": "tsc --watch",
29
+ "lint": "eslint src tests scripts",
30
+ "lint:fix": "eslint src tests scripts --fix",
31
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.ts\"",
32
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.ts\"",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
36
+ "start": "node dist/cli.js",
37
+ "bun-compile": "bun build ./src/cli.ts --compile --external chromium-bidi --external electron --outfile latchkey"
38
+ },
39
+ "keywords": [
40
+ "cli",
41
+ "curl",
42
+ "api",
43
+ "credentials",
44
+ "authentication",
45
+ "agents",
46
+ "imbue"
47
+ ],
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "dependencies": {
53
+ "@napi-rs/keyring": "^1.2.0",
54
+ "commander": "^12.0.0",
55
+ "playwright": "^1.58.2",
56
+ "zod": "^3.22.0"
57
+ },
58
+ "devDependencies": {
59
+ "@eslint/js": "^9.39.2",
60
+ "@types/node": "^20.0.0",
61
+ "eslint": "^9.39.2",
62
+ "prettier": "^3.8.1",
63
+ "typescript": "^5.3.0",
64
+ "typescript-eslint": "^8.53.1",
65
+ "vitest": "^2.0.0"
66
+ }
67
+ }
@@ -0,0 +1,21 @@
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/encryptFile.ts decrypt <file> # Decrypt file to stdout
10
+ * npx tsx scripts/encryptFile.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/encryptFile.ts decrypt ~/.latchkey/credentials.json
18
+ * npx tsx scripts/encryptFile.ts encrypt ~/.latchkey/credentials.json
19
+ */
20
+ export {};
21
+ //# sourceMappingURL=encryptFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptFile.d.ts","sourceRoot":"","sources":["../../scripts/encryptFile.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -0,0 +1,101 @@
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/encryptFile.ts decrypt <file> # Decrypt file to stdout
10
+ * npx tsx scripts/encryptFile.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/encryptFile.ts decrypt ~/.latchkey/credentials.json
18
+ * npx tsx scripts/encryptFile.ts encrypt ~/.latchkey/credentials.json
19
+ */
20
+ import { program } from 'commander';
21
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
22
+ import { CONFIG } from '../src/config.js';
23
+ import { EncryptedStorage } from '../src/encryptedStorage.js';
24
+ import { encrypt, generateKey } from '../src/encryption.js';
25
+ import { isKeychainAvailable, retrieveFromKeychain } from '../src/keychain.js';
26
+ const ENCRYPTED_FILE_PREFIX = 'LATCHKEY_ENCRYPTED:';
27
+ function getEncryptionKey() {
28
+ // 1. Check environment variable via Config
29
+ if (CONFIG.encryptionKeyOverride) {
30
+ return CONFIG.encryptionKeyOverride;
31
+ }
32
+ // 2. Check keychain
33
+ if (isKeychainAvailable(CONFIG.serviceName, CONFIG.accountName)) {
34
+ const keychainKey = retrieveFromKeychain(CONFIG.serviceName, CONFIG.accountName);
35
+ if (keychainKey) {
36
+ return keychainKey;
37
+ }
38
+ }
39
+ console.error(`\
40
+ Error: No encryption key available.
41
+ Set LATCHKEY_ENCRYPTION_KEY or ensure the system keychain has a stored key.
42
+
43
+ To generate a new key:
44
+ export LATCHKEY_ENCRYPTION_KEY="${generateKey()}"`);
45
+ process.exit(1);
46
+ }
47
+ function decryptCommand(filePath) {
48
+ if (!existsSync(filePath)) {
49
+ console.error(`Error: File not found: ${filePath}`);
50
+ process.exit(1);
51
+ }
52
+ const storage = new EncryptedStorage({
53
+ serviceName: CONFIG.serviceName,
54
+ accountName: CONFIG.accountName,
55
+ });
56
+ const content = storage.readFile(filePath);
57
+ if (content === null) {
58
+ console.error(`Error: Could not read file: ${filePath}`);
59
+ process.exit(1);
60
+ }
61
+ // Output to stdout
62
+ process.stdout.write(content);
63
+ }
64
+ function encryptCommand(filePath) {
65
+ if (!existsSync(filePath)) {
66
+ console.error(`Error: File not found: ${filePath}`);
67
+ process.exit(1);
68
+ }
69
+ const content = readFileSync(filePath, 'utf-8');
70
+ if (content.startsWith(ENCRYPTED_FILE_PREFIX)) {
71
+ console.error(`Error: File is already encrypted: ${filePath}`);
72
+ process.exit(1);
73
+ }
74
+ const key = getEncryptionKey();
75
+ const encryptedData = encrypt(content, key);
76
+ const dataToWrite = ENCRYPTED_FILE_PREFIX + encryptedData;
77
+ writeFileSync(filePath, dataToWrite, { encoding: 'utf-8', mode: 0o600 });
78
+ console.error(`Encrypted: ${filePath}`);
79
+ }
80
+ program.name('encryptFile').description(`\
81
+ CLI tool for encrypting and decrypting latchkey files.
82
+
83
+ The encryption key is sourced from:
84
+ 1. LATCHKEY_ENCRYPTION_KEY environment variable
85
+ 2. System keychain`);
86
+ program
87
+ .command('decrypt')
88
+ .description('Decrypt file and print to stdout')
89
+ .argument('<file>', 'Path to the encrypted file')
90
+ .action((filePath) => {
91
+ decryptCommand(filePath);
92
+ });
93
+ program
94
+ .command('encrypt')
95
+ .description('Encrypt an unencrypted file in place')
96
+ .argument('<file>', 'Path to the file to encrypt')
97
+ .action((filePath) => {
98
+ encryptCommand(filePath);
99
+ });
100
+ program.parse();
101
+ //# sourceMappingURL=encryptFile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptFile.js","sourceRoot":"","sources":["../../scripts/encryptFile.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ export declare class ApiCredentialStore {
14
14
  private saveStoreData;
15
15
  get(serviceName: string): ApiCredentials | null;
16
16
  save(serviceName: string, apiCredentials: ApiCredentials): void;
17
+ getAll(): ReadonlyMap<string, ApiCredentials>;
17
18
  delete(serviceName: string): boolean;
18
19
  }
19
20
  //# sourceMappingURL=apiCredentialStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apiCredentialStore.d.ts","sourceRoot":"","sources":["../../src/apiCredentialStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,cAAc,EAIf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAID,qBAAa,kBAAkB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;gBAExC,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,gBAAgB;IAK5D,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,aAAa;IAUrB,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAiB/C,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,GAAG,IAAI;IAM/D,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;CASrC"}
1
+ {"version":3,"file":"apiCredentialStore.d.ts","sourceRoot":"","sources":["../../src/apiCredentialStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,cAAc,EAIf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAID,qBAAa,kBAAkB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;gBAExC,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,gBAAgB;IAK5D,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,aAAa;IAUrB,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAiB/C,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,GAAG,IAAI;IAM/D,MAAM,IAAI,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC;IAe7C,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;CASrC"}
@@ -52,6 +52,18 @@ export class ApiCredentialStore {
52
52
  data[serviceName] = serializeCredentials(apiCredentials);
53
53
  this.saveStoreData(data);
54
54
  }
55
+ getAll() {
56
+ const data = this.loadStoreData();
57
+ const result = new Map();
58
+ for (const [serviceName, credentialData] of Object.entries(data)) {
59
+ const parseResult = ApiCredentialsSchema.safeParse(credentialData);
60
+ if (!parseResult.success) {
61
+ throw new ApiCredentialStoreError(`Invalid credential data for service ${serviceName}: ${parseResult.error.message}`);
62
+ }
63
+ result.set(serviceName, deserializeCredentials(parseResult.data));
64
+ }
65
+ return result;
66
+ }
55
67
  delete(serviceName) {
56
68
  const data = this.loadStoreData();
57
69
  if (!(serviceName in data)) {
@@ -1 +1 @@
1
- {"version":3,"file":"apiCredentialStore.js","sourceRoot":"","sources":["../../src/apiCredentialStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"apiCredentialStore.js","sourceRoot":"","sources":["../../src/apiCredentialStore.ts"],"names":[],"mappings":"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"}