latchkey 0.1.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 (176) hide show
  1. package/.nvmrc +1 -0
  2. package/.pre-commit-config.yaml +22 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +7 -0
  5. package/CLAUDE.md +13 -0
  6. package/LICENSE +7 -0
  7. package/README.md +167 -0
  8. package/dist/scripts/cryptFile.d.ts +21 -0
  9. package/dist/scripts/cryptFile.d.ts.map +1 -0
  10. package/dist/scripts/cryptFile.js +106 -0
  11. package/dist/scripts/cryptFile.js.map +1 -0
  12. package/dist/scripts/encryptFile.d.ts +21 -0
  13. package/dist/scripts/encryptFile.d.ts.map +1 -0
  14. package/dist/scripts/encryptFile.js +101 -0
  15. package/dist/scripts/encryptFile.js.map +1 -0
  16. package/dist/scripts/recordBrowserSession.d.ts +18 -0
  17. package/dist/scripts/recordBrowserSession.d.ts.map +1 -0
  18. package/dist/scripts/recordBrowserSession.js +213 -0
  19. package/dist/scripts/recordBrowserSession.js.map +1 -0
  20. package/dist/src/apiCredentialStore.d.ts +19 -0
  21. package/dist/src/apiCredentialStore.d.ts.map +1 -0
  22. package/dist/src/apiCredentialStore.js +65 -0
  23. package/dist/src/apiCredentialStore.js.map +1 -0
  24. package/dist/src/apiCredentials.d.ts +134 -0
  25. package/dist/src/apiCredentials.d.ts.map +1 -0
  26. package/dist/src/apiCredentials.js +139 -0
  27. package/dist/src/apiCredentials.js.map +1 -0
  28. package/dist/src/browserConfig.d.ts +90 -0
  29. package/dist/src/browserConfig.d.ts.map +1 -0
  30. package/dist/src/browserConfig.js +259 -0
  31. package/dist/src/browserConfig.js.map +1 -0
  32. package/dist/src/browserState.d.ts +8 -0
  33. package/dist/src/browserState.d.ts.map +1 -0
  34. package/dist/src/browserState.js +21 -0
  35. package/dist/src/browserState.js.map +1 -0
  36. package/dist/src/cli.d.ts +6 -0
  37. package/dist/src/cli.d.ts.map +1 -0
  38. package/dist/src/cli.js +25 -0
  39. package/dist/src/cli.js.map +1 -0
  40. package/dist/src/cliCommands.d.ts +29 -0
  41. package/dist/src/cliCommands.d.ts.map +1 -0
  42. package/dist/src/cliCommands.js +264 -0
  43. package/dist/src/cliCommands.js.map +1 -0
  44. package/dist/src/config.d.ts +35 -0
  45. package/dist/src/config.d.ts.map +1 -0
  46. package/dist/src/config.js +96 -0
  47. package/dist/src/config.js.map +1 -0
  48. package/dist/src/curl.d.ts +29 -0
  49. package/dist/src/curl.d.ts.map +1 -0
  50. package/dist/src/curl.js +53 -0
  51. package/dist/src/curl.js.map +1 -0
  52. package/dist/src/encryptedStorage.d.ts +39 -0
  53. package/dist/src/encryptedStorage.d.ts.map +1 -0
  54. package/dist/src/encryptedStorage.js +128 -0
  55. package/dist/src/encryptedStorage.js.map +1 -0
  56. package/dist/src/encryption.d.ts +28 -0
  57. package/dist/src/encryption.d.ts.map +1 -0
  58. package/dist/src/encryption.js +86 -0
  59. package/dist/src/encryption.js.map +1 -0
  60. package/dist/src/index.d.ts +14 -0
  61. package/dist/src/index.d.ts.map +1 -0
  62. package/dist/src/index.js +17 -0
  63. package/dist/src/index.js.map +1 -0
  64. package/dist/src/keychain.d.ts +33 -0
  65. package/dist/src/keychain.d.ts.map +1 -0
  66. package/dist/src/keychain.js +94 -0
  67. package/dist/src/keychain.js.map +1 -0
  68. package/dist/src/playwrightUtils.d.ts +27 -0
  69. package/dist/src/playwrightUtils.d.ts.map +1 -0
  70. package/dist/src/playwrightUtils.js +122 -0
  71. package/dist/src/playwrightUtils.js.map +1 -0
  72. package/dist/src/registry.d.ts +12 -0
  73. package/dist/src/registry.d.ts.map +1 -0
  74. package/dist/src/registry.js +30 -0
  75. package/dist/src/registry.js.map +1 -0
  76. package/dist/src/services/base.d.ts +98 -0
  77. package/dist/src/services/base.d.ts.map +1 -0
  78. package/dist/src/services/base.js +137 -0
  79. package/dist/src/services/base.js.map +1 -0
  80. package/dist/src/services/discord.d.ts +20 -0
  81. package/dist/src/services/discord.d.ts.map +1 -0
  82. package/dist/src/services/discord.js +55 -0
  83. package/dist/src/services/discord.js.map +1 -0
  84. package/dist/src/services/dropbox.d.ts +23 -0
  85. package/dist/src/services/dropbox.d.ts.map +1 -0
  86. package/dist/src/services/dropbox.js +136 -0
  87. package/dist/src/services/dropbox.js.map +1 -0
  88. package/dist/src/services/github.d.ts +23 -0
  89. package/dist/src/services/github.d.ts.map +1 -0
  90. package/dist/src/services/github.js +110 -0
  91. package/dist/src/services/github.js.map +1 -0
  92. package/dist/src/services/index.d.ts +12 -0
  93. package/dist/src/services/index.d.ts.map +1 -0
  94. package/dist/src/services/index.js +11 -0
  95. package/dist/src/services/index.js.map +1 -0
  96. package/dist/src/services/linear.d.ts +23 -0
  97. package/dist/src/services/linear.d.ts.map +1 -0
  98. package/dist/src/services/linear.js +110 -0
  99. package/dist/src/services/linear.js.map +1 -0
  100. package/dist/src/services/slack.d.ts +21 -0
  101. package/dist/src/services/slack.d.ts.map +1 -0
  102. package/dist/src/services/slack.js +67 -0
  103. package/dist/src/services/slack.js.map +1 -0
  104. package/dist/tests/apiCredentialStore.test.d.ts +2 -0
  105. package/dist/tests/apiCredentialStore.test.d.ts.map +1 -0
  106. package/dist/tests/apiCredentialStore.test.js +130 -0
  107. package/dist/tests/apiCredentialStore.test.js.map +1 -0
  108. package/dist/tests/apiCredentials.test.d.ts +2 -0
  109. package/dist/tests/apiCredentials.test.d.ts.map +1 -0
  110. package/dist/tests/apiCredentials.test.js +169 -0
  111. package/dist/tests/apiCredentials.test.js.map +1 -0
  112. package/dist/tests/cli.test.d.ts +2 -0
  113. package/dist/tests/cli.test.d.ts.map +1 -0
  114. package/dist/tests/cli.test.js +584 -0
  115. package/dist/tests/cli.test.js.map +1 -0
  116. package/dist/tests/encryptedStorage.test.d.ts +2 -0
  117. package/dist/tests/encryptedStorage.test.d.ts.map +1 -0
  118. package/dist/tests/encryptedStorage.test.js +126 -0
  119. package/dist/tests/encryptedStorage.test.js.map +1 -0
  120. package/dist/tests/encryption.test.d.ts +2 -0
  121. package/dist/tests/encryption.test.d.ts.map +1 -0
  122. package/dist/tests/encryption.test.js +121 -0
  123. package/dist/tests/encryption.test.js.map +1 -0
  124. package/dist/tests/lint.test.d.ts +2 -0
  125. package/dist/tests/lint.test.d.ts.map +1 -0
  126. package/dist/tests/lint.test.js +18 -0
  127. package/dist/tests/lint.test.js.map +1 -0
  128. package/dist/tests/registry.test.d.ts +2 -0
  129. package/dist/tests/registry.test.d.ts.map +1 -0
  130. package/dist/tests/registry.test.js +85 -0
  131. package/dist/tests/registry.test.js.map +1 -0
  132. package/dist/tests/servicesAgainstRecordings.test.d.ts +20 -0
  133. package/dist/tests/servicesAgainstRecordings.test.d.ts.map +1 -0
  134. package/dist/tests/servicesAgainstRecordings.test.js +157 -0
  135. package/dist/tests/servicesAgainstRecordings.test.js.map +1 -0
  136. package/dist/tests/typecheck.test.d.ts +2 -0
  137. package/dist/tests/typecheck.test.d.ts.map +1 -0
  138. package/dist/tests/typecheck.test.js +18 -0
  139. package/dist/tests/typecheck.test.js.map +1 -0
  140. package/docs/development.md +94 -0
  141. package/eslint.config.js +30 -0
  142. package/integrations/SKILL.md +62 -0
  143. package/package.json +68 -0
  144. package/scripts/cryptFile.ts +123 -0
  145. package/scripts/recordBrowserSession.ts +280 -0
  146. package/scripts/tsconfig.json +10 -0
  147. package/src/apiCredentialStore.ts +87 -0
  148. package/src/apiCredentials.ts +180 -0
  149. package/src/cli.ts +32 -0
  150. package/src/cliCommands.ts +321 -0
  151. package/src/config.ts +115 -0
  152. package/src/curl.ts +78 -0
  153. package/src/encryptedStorage.ts +161 -0
  154. package/src/encryption.ts +106 -0
  155. package/src/index.ts +65 -0
  156. package/src/keychain.ts +105 -0
  157. package/src/playwrightUtils.ts +143 -0
  158. package/src/registry.ts +35 -0
  159. package/src/services/base.ts +234 -0
  160. package/src/services/discord.ts +73 -0
  161. package/src/services/dropbox.ts +173 -0
  162. package/src/services/github.ts +139 -0
  163. package/src/services/index.ts +13 -0
  164. package/src/services/linear.ts +134 -0
  165. package/src/services/slack.ts +85 -0
  166. package/tests/apiCredentialStore.test.ts +162 -0
  167. package/tests/apiCredentials.test.ts +195 -0
  168. package/tests/cli.test.ts +798 -0
  169. package/tests/encryptedStorage.test.ts +173 -0
  170. package/tests/encryption.test.ts +169 -0
  171. package/tests/lint.test.ts +19 -0
  172. package/tests/registry.test.ts +103 -0
  173. package/tests/servicesAgainstRecordings.test.ts +230 -0
  174. package/tests/typecheck.test.ts +19 -0
  175. package/tsconfig.json +24 -0
  176. package/vitest.config.ts +13 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Encrypted file storage with automatic key management.
3
+ * Encryption keys are retrieved from:
4
+ * 1. Provided encryptionKeyOverride option
5
+ * 2. System keychain
6
+ * 3. Generated and stored in keychain (first run)
7
+ *
8
+ * Falls back to unencrypted storage with chmod 600 if keychain is unavailable.
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
11
+ import { dirname } from 'node:path';
12
+ import { DEFAULT_KEYRING_SERVICE_NAME, DEFAULT_KEYRING_ACCOUNT_NAME } from './config.js';
13
+ import { encrypt, decrypt, generateKey, DecryptionError } from './encryption.js';
14
+ import { isKeychainAvailable, retrieveFromKeychain, storeInKeychain, KeychainNotAvailableError, } from './keychain.js';
15
+ const ENCRYPTED_FILE_PREFIX = 'LATCHKEY_ENCRYPTED:';
16
+ export class EncryptedStorageError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'EncryptedStorageError';
20
+ }
21
+ }
22
+ export class PathIsDirectoryError extends Error {
23
+ constructor(filePath) {
24
+ super(`Path is a directory, not a file: ${filePath}`);
25
+ this.name = 'PathIsDirectoryError';
26
+ }
27
+ }
28
+ /**
29
+ * Manages encrypted file storage with automatic key handling.
30
+ */
31
+ export class EncryptedStorage {
32
+ key;
33
+ constructor(options = {}) {
34
+ this.key = EncryptedStorage.initializeKey(options);
35
+ }
36
+ static initializeKey(options) {
37
+ // If key was provided via override, use it
38
+ if (options.encryptionKeyOverride !== undefined && options.encryptionKeyOverride !== null) {
39
+ return options.encryptionKeyOverride;
40
+ }
41
+ const serviceName = options.serviceName ?? DEFAULT_KEYRING_SERVICE_NAME;
42
+ const accountName = options.accountName ?? DEFAULT_KEYRING_ACCOUNT_NAME;
43
+ // Check if keychain is available
44
+ if (!isKeychainAvailable(serviceName, accountName)) {
45
+ return null;
46
+ }
47
+ // Try to retrieve from keychain
48
+ try {
49
+ const keychainKey = retrieveFromKeychain(serviceName, accountName);
50
+ if (keychainKey) {
51
+ return keychainKey;
52
+ }
53
+ // Generate new key and store in keychain
54
+ const newKey = generateKey();
55
+ storeInKeychain(serviceName, accountName, newKey);
56
+ return newKey;
57
+ }
58
+ catch (error) {
59
+ if (error instanceof KeychainNotAvailableError) {
60
+ // Fall back to unencrypted storage
61
+ return null;
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ isEncryptionEnabled() {
67
+ return this.key !== null;
68
+ }
69
+ /**
70
+ * Read and decrypt a file.
71
+ */
72
+ readFile(filePath) {
73
+ if (!existsSync(filePath)) {
74
+ return null;
75
+ }
76
+ const stats = statSync(filePath);
77
+ if (stats.isDirectory()) {
78
+ throw new PathIsDirectoryError(filePath);
79
+ }
80
+ const content = readFileSync(filePath, 'utf-8');
81
+ // Check if the file is encrypted
82
+ if (content.startsWith(ENCRYPTED_FILE_PREFIX)) {
83
+ if (this.key === null) {
84
+ throw new EncryptedStorageError('File is encrypted but a key is not available. ' +
85
+ 'Set LATCHKEY_ENCRYPTION_KEY or ensure system keychain is accessible.');
86
+ }
87
+ const encryptedData = content.slice(ENCRYPTED_FILE_PREFIX.length);
88
+ try {
89
+ return decrypt(encryptedData, this.key);
90
+ }
91
+ catch (error) {
92
+ if (error instanceof DecryptionError) {
93
+ throw new EncryptedStorageError(`Failed to decrypt file: ${error.message}. ` + 'The encryption key may have changed.');
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ // File is not encrypted (fallback mode when keychain unavailable)
99
+ return content;
100
+ }
101
+ /**
102
+ * Encrypt and write data to a file.
103
+ * Creates parent directories. New files are created with chmod 600.
104
+ */
105
+ writeFile(filePath, content) {
106
+ const dir = dirname(filePath);
107
+ if (!existsSync(dir)) {
108
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
109
+ }
110
+ if (existsSync(filePath)) {
111
+ const stats = statSync(filePath);
112
+ if (stats.isDirectory()) {
113
+ throw new PathIsDirectoryError(filePath);
114
+ }
115
+ }
116
+ let dataToWrite;
117
+ if (this.key !== null) {
118
+ const encryptedData = encrypt(content, this.key);
119
+ dataToWrite = ENCRYPTED_FILE_PREFIX + encryptedData;
120
+ }
121
+ else {
122
+ // Fallback to unencrypted storage
123
+ dataToWrite = content;
124
+ }
125
+ writeFileSync(filePath, dataToWrite, { encoding: 'utf-8', mode: 0o600 });
126
+ }
127
+ }
128
+ //# sourceMappingURL=encryptedStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptedStorage.js","sourceRoot":"","sources":["../../src/encryptedStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Encryption utilities for secure credential storage.
3
+ * Uses AES-256-GCM for authenticated encryption.
4
+ */
5
+ export declare class EncryptionError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ export declare class DecryptionError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /**
12
+ * Encrypt data using AES-256-GCM.
13
+ * The key should be a base64-encoded 256-bit key.
14
+ * Returns a base64-encoded string containing: iv + authTag + ciphertext
15
+ */
16
+ export declare function encrypt(plaintext: string, keyBase64: string): string;
17
+ /**
18
+ * Decrypt data that was encrypted with the encrypt function.
19
+ * The key should be a base64-encoded 256-bit key.
20
+ * Input should be a base64-encoded string containing: iv + authTag + ciphertext
21
+ */
22
+ export declare function decrypt(encryptedData: string, keyBase64: string): string;
23
+ /**
24
+ * Generate a cryptographically secure random 256-bit key.
25
+ * Returns the key as a base64-encoded string.
26
+ */
27
+ export declare function generateKey(): string;
28
+ //# sourceMappingURL=encryption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../src/encryption.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CA0BpE;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiCxE;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Encryption utilities for secure credential storage.
3
+ * Uses AES-256-GCM for authenticated encryption.
4
+ */
5
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
6
+ const ALGORITHM = 'aes-256-gcm';
7
+ const KEY_LENGTH = 32; // 256 bits
8
+ const IV_LENGTH = 12; // 96 bits for GCM
9
+ const AUTH_TAG_LENGTH = 16; // 128 bits
10
+ export class EncryptionError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = 'EncryptionError';
14
+ }
15
+ }
16
+ export class DecryptionError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'DecryptionError';
20
+ }
21
+ }
22
+ /**
23
+ * Encrypt data using AES-256-GCM.
24
+ * The key should be a base64-encoded 256-bit key.
25
+ * Returns a base64-encoded string containing: iv + authTag + ciphertext
26
+ */
27
+ export function encrypt(plaintext, keyBase64) {
28
+ try {
29
+ const key = Buffer.from(keyBase64, 'base64');
30
+ if (key.length !== KEY_LENGTH) {
31
+ throw new EncryptionError(`Invalid key length: expected ${String(KEY_LENGTH)} bytes, got ${String(key.length)}`);
32
+ }
33
+ const iv = randomBytes(IV_LENGTH);
34
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
35
+ const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
36
+ const authTag = cipher.getAuthTag();
37
+ // Combine: iv (12) + authTag (16) + ciphertext
38
+ const combined = Buffer.concat([iv, authTag, ciphertext]);
39
+ return combined.toString('base64');
40
+ }
41
+ catch (error) {
42
+ if (error instanceof EncryptionError) {
43
+ throw error;
44
+ }
45
+ throw new EncryptionError(`Failed to encrypt data: ${error instanceof Error ? error.message : String(error)}`);
46
+ }
47
+ }
48
+ /**
49
+ * Decrypt data that was encrypted with the encrypt function.
50
+ * The key should be a base64-encoded 256-bit key.
51
+ * Input should be a base64-encoded string containing: iv + authTag + ciphertext
52
+ */
53
+ export function decrypt(encryptedData, keyBase64) {
54
+ try {
55
+ const key = Buffer.from(keyBase64, 'base64');
56
+ if (key.length !== KEY_LENGTH) {
57
+ throw new DecryptionError(`Invalid key length: expected ${String(KEY_LENGTH)} bytes, got ${String(key.length)}`);
58
+ }
59
+ const combined = Buffer.from(encryptedData, 'base64');
60
+ // Minimum length is iv + authTag (ciphertext can be empty for empty string)
61
+ if (combined.length < IV_LENGTH + AUTH_TAG_LENGTH) {
62
+ throw new DecryptionError('Invalid encrypted data: too short');
63
+ }
64
+ const iv = combined.subarray(0, IV_LENGTH);
65
+ const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
66
+ const ciphertext = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
67
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
68
+ decipher.setAuthTag(authTag);
69
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
70
+ return decrypted.toString('utf8');
71
+ }
72
+ catch (error) {
73
+ if (error instanceof DecryptionError) {
74
+ throw error;
75
+ }
76
+ throw new DecryptionError(`Failed to decrypt data: ${error instanceof Error ? error.message : String(error)}`);
77
+ }
78
+ }
79
+ /**
80
+ * Generate a cryptographically secure random 256-bit key.
81
+ * Returns the key as a base64-encoded string.
82
+ */
83
+ export function generateKey() {
84
+ return randomBytes(KEY_LENGTH).toString('base64');
85
+ }
86
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../../src/encryption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Latchkey - A CLI tool that injects API credentials into curl requests.
3
+ */
4
+ export { ApiCredentials, ApiCredentialStatus, AuthorizationBearer, AuthorizationBare, SlackApiCredentials, deserializeCredentials, serializeCredentials, } from './apiCredentials.js';
5
+ export { ApiCredentialStore, ApiCredentialStoreError } from './apiCredentialStore.js';
6
+ export { Config, CONFIG, InsecureFilePermissionsError } from './config.js';
7
+ export { encrypt, decrypt, generateKey, EncryptionError, DecryptionError } from './encryption.js';
8
+ export { EncryptedStorage, EncryptedStorageError } from './encryptedStorage.js';
9
+ export { storeInKeychain, retrieveFromKeychain, deleteFromKeychain, isKeychainAvailable, KeychainError, KeychainNotAvailableError, } from './keychain.js';
10
+ export { run as runCurl, runCaptured as runCurlCaptured, setSubprocessRunner, resetSubprocessRunner, setCapturingSubprocessRunner, resetCapturingSubprocessRunner, } from './curl.js';
11
+ export { typeLikeHuman } from './playwrightUtils.js';
12
+ export { Service, ServiceSession, SimpleServiceSession, BrowserFollowupServiceSession, LoginCancelledError, LoginFailedError, Slack, SLACK, Discord, DISCORD, Github, GITHUB, Dropbox, DROPBOX, Linear, LINEAR, } from './services/index.js';
13
+ export { Registry, REGISTRY } from './registry.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEtF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElG,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEhF,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,GAAG,IAAI,OAAO,EACd,WAAW,IAAI,eAAe,EAC9B,mBAAmB,EACnB,qBAAqB,EACrB,4BAA4B,EAC5B,8BAA8B,GAC/B,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGrD,OAAO,EACL,OAAO,EACP,cAAc,EACd,oBAAoB,EACpB,6BAA6B,EAC7B,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAM,GACP,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Latchkey - A CLI tool that injects API credentials into curl requests.
3
+ */
4
+ // Core types and utilities
5
+ export { ApiCredentialStatus, AuthorizationBearer, AuthorizationBare, SlackApiCredentials, deserializeCredentials, serializeCredentials, } from './apiCredentials.js';
6
+ export { ApiCredentialStore, ApiCredentialStoreError } from './apiCredentialStore.js';
7
+ export { Config, CONFIG, InsecureFilePermissionsError } from './config.js';
8
+ export { encrypt, decrypt, generateKey, EncryptionError, DecryptionError } from './encryption.js';
9
+ export { EncryptedStorage, EncryptedStorageError } from './encryptedStorage.js';
10
+ export { storeInKeychain, retrieveFromKeychain, deleteFromKeychain, isKeychainAvailable, KeychainError, KeychainNotAvailableError, } from './keychain.js';
11
+ export { run as runCurl, runCaptured as runCurlCaptured, setSubprocessRunner, resetSubprocessRunner, setCapturingSubprocessRunner, resetCapturingSubprocessRunner, } from './curl.js';
12
+ export { typeLikeHuman } from './playwrightUtils.js';
13
+ // Services
14
+ export { ServiceSession, SimpleServiceSession, BrowserFollowupServiceSession, LoginCancelledError, LoginFailedError, Slack, SLACK, Discord, DISCORD, Github, GITHUB, Dropbox, DROPBOX, Linear, LINEAR, } from './services/index.js';
15
+ // Registry
16
+ export { Registry, REGISTRY } from './registry.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,2BAA2B;AAC3B,OAAO,EAEL,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEtF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElG,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEhF,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,GAAG,IAAI,OAAO,EACd,WAAW,IAAI,eAAe,EAC9B,mBAAmB,EACnB,qBAAqB,EACrB,4BAA4B,EAC5B,8BAA8B,GAC/B,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,WAAW;AACX,OAAO,EAEL,cAAc,EACd,oBAAoB,EACpB,6BAA6B,EAC7B,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAM,GACP,MAAM,qBAAqB,CAAC;AAE7B,WAAW;AACX,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * System keychain integration for secure password storage.
3
+ * Uses @napi-rs/keyring for cross-platform support (macOS Keychain, Windows Credential Manager,
4
+ * Linux Secret Service via keyutils/kernel keyring).
5
+ */
6
+ export declare class KeychainError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ export declare class KeychainNotAvailableError extends KeychainError {
10
+ constructor(message: string);
11
+ }
12
+ /**
13
+ * Store a password in the system keychain.
14
+ * Throws KeychainNotAvailableError if the keychain is not accessible.
15
+ */
16
+ export declare function storeInKeychain(serviceName: string, accountName: string, password: string): void;
17
+ /**
18
+ * Retrieve a password from the system keychain.
19
+ * Returns null if the password is not found.
20
+ * Throws KeychainNotAvailableError if the keychain is not accessible.
21
+ */
22
+ export declare function retrieveFromKeychain(serviceName: string, accountName: string): string | null;
23
+ /**
24
+ * Delete a password from the system keychain.
25
+ * Returns true if deleted, false if not found.
26
+ * Throws KeychainNotAvailableError if the keychain is not accessible.
27
+ */
28
+ export declare function deleteFromKeychain(serviceName: string, accountName: string): boolean;
29
+ /**
30
+ * Check if the system keychain is available.
31
+ */
32
+ export declare function isKeychainAvailable(serviceName: string, accountName: string): boolean;
33
+ //# sourceMappingURL=keychain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.d.ts","sourceRoot":"","sources":["../../src/keychain.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,aAAa;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AASD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAShG;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmB5F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAgBpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAQrF"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * System keychain integration for secure password storage.
3
+ * Uses @napi-rs/keyring for cross-platform support (macOS Keychain, Windows Credential Manager,
4
+ * Linux Secret Service via keyutils/kernel keyring).
5
+ */
6
+ import { Entry } from '@napi-rs/keyring';
7
+ export class KeychainError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'KeychainError';
11
+ }
12
+ }
13
+ export class KeychainNotAvailableError extends KeychainError {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'KeychainNotAvailableError';
17
+ }
18
+ }
19
+ /**
20
+ * Get a keyring entry.
21
+ */
22
+ function getEntry(serviceName, accountName) {
23
+ return new Entry(serviceName, accountName);
24
+ }
25
+ /**
26
+ * Store a password in the system keychain.
27
+ * Throws KeychainNotAvailableError if the keychain is not accessible.
28
+ */
29
+ export function storeInKeychain(serviceName, accountName, password) {
30
+ try {
31
+ const entry = getEntry(serviceName, accountName);
32
+ entry.setPassword(password);
33
+ }
34
+ catch (error) {
35
+ throw new KeychainNotAvailableError(`Failed to store password in keychain: ${error instanceof Error ? error.message : String(error)}`);
36
+ }
37
+ }
38
+ /**
39
+ * Retrieve a password from the system keychain.
40
+ * Returns null if the password is not found.
41
+ * Throws KeychainNotAvailableError if the keychain is not accessible.
42
+ */
43
+ export function retrieveFromKeychain(serviceName, accountName) {
44
+ try {
45
+ const entry = getEntry(serviceName, accountName);
46
+ const password = entry.getPassword();
47
+ return password ?? null;
48
+ }
49
+ catch (error) {
50
+ const errorMessage = error instanceof Error ? error.message : String(error);
51
+ // Check if it's a "not found" error
52
+ if (errorMessage.includes('not found') ||
53
+ errorMessage.includes('No password') ||
54
+ errorMessage.includes('ItemNotFound')) {
55
+ return null;
56
+ }
57
+ throw new KeychainNotAvailableError(`Failed to retrieve password from keychain: ${errorMessage}`);
58
+ }
59
+ }
60
+ /**
61
+ * Delete a password from the system keychain.
62
+ * Returns true if deleted, false if not found.
63
+ * Throws KeychainNotAvailableError if the keychain is not accessible.
64
+ */
65
+ export function deleteFromKeychain(serviceName, accountName) {
66
+ try {
67
+ const entry = getEntry(serviceName, accountName);
68
+ entry.deletePassword();
69
+ return true;
70
+ }
71
+ catch (error) {
72
+ const errorMessage = error instanceof Error ? error.message : String(error);
73
+ if (errorMessage.includes('not found') ||
74
+ errorMessage.includes('No password') ||
75
+ errorMessage.includes('ItemNotFound')) {
76
+ return false;
77
+ }
78
+ throw new KeychainNotAvailableError(`Failed to delete password from keychain: ${errorMessage}`);
79
+ }
80
+ }
81
+ /**
82
+ * Check if the system keychain is available.
83
+ */
84
+ export function isKeychainAvailable(serviceName, accountName) {
85
+ try {
86
+ // Try to create an entry - this should work on all supported platforms
87
+ getEntry(serviceName, accountName);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../src/keychain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Playwright utility functions for browser automation.
3
+ */
4
+ import type { Browser, BrowserContext, Page, Locator } from 'playwright';
5
+ import { EncryptedStorage } from './encryptedStorage.js';
6
+ export interface BrowserWithContext {
7
+ readonly browser: Browser;
8
+ readonly context: BrowserContext;
9
+ }
10
+ /**
11
+ * Run a callback with a browser context initialized from encrypted storage state.
12
+ * After the callback completes, persists browser state back to encrypted storage.
13
+ */
14
+ export declare function withTempBrowserContext<T>(encryptedStorage: EncryptedStorage, browserStatePath: string, callback: (state: BrowserWithContext) => Promise<T>): Promise<T>;
15
+ /**
16
+ * Type text character by character with random delays to simulate human typing.
17
+ *
18
+ * This triggers proper JavaScript input events that some websites require,
19
+ * unlike fill() which sets the value directly.
20
+ */
21
+ export declare function typeLikeHuman(page: Page, locator: Locator, text: string): Promise<void>;
22
+ /**
23
+ * Show a spinner overlay that hides page content from the user.
24
+ * The overlay persists across page navigations within the browser context.
25
+ */
26
+ export declare function showSpinnerPage(context: BrowserContext, serviceName: string): Promise<void>;
27
+ //# sourceMappingURL=playwrightUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwrightUtils.d.ts","sourceRoot":"","sources":["../../src/playwrightUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,gBAAgB,EAAE,gBAAgB,EAClC,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAClD,OAAO,CAAC,CAAC,CAAC,CAuCZ;AAMD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS7F;AAmDD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjG"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Playwright utility functions for browser automation.
3
+ */
4
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ /**
8
+ * Run a callback with a browser context initialized from encrypted storage state.
9
+ * After the callback completes, persists browser state back to encrypted storage.
10
+ */
11
+ export async function withTempBrowserContext(encryptedStorage, browserStatePath, callback) {
12
+ const tempDir = mkdtempSync(join(tmpdir(), 'latchkey-browser-state-'));
13
+ const tempFilePath = join(tempDir, 'browser_state.json');
14
+ let initialStorageState;
15
+ if (existsSync(browserStatePath)) {
16
+ const content = encryptedStorage.readFile(browserStatePath);
17
+ if (content !== null) {
18
+ writeFileSync(tempFilePath, content, { encoding: 'utf-8', mode: 0o600 });
19
+ initialStorageState = tempFilePath;
20
+ }
21
+ }
22
+ const { chromium: chromiumBrowser } = await import('playwright');
23
+ const browser = await chromiumBrowser.launch({ headless: false });
24
+ try {
25
+ const contextOptions = {};
26
+ if (initialStorageState !== undefined) {
27
+ contextOptions.storageState = initialStorageState;
28
+ }
29
+ const context = await browser.newContext(contextOptions);
30
+ const result = await callback({ browser, context });
31
+ // Persist browser state back to encrypted storage
32
+ await context.storageState({ path: tempFilePath });
33
+ const content = readFileSync(tempFilePath, 'utf-8');
34
+ encryptedStorage.writeFile(browserStatePath, content);
35
+ return result;
36
+ }
37
+ finally {
38
+ await browser.close();
39
+ try {
40
+ rmSync(tempDir, { recursive: true, force: true });
41
+ }
42
+ catch {
43
+ // Ignore cleanup errors
44
+ }
45
+ }
46
+ }
47
+ // Typing delay range in milliseconds (min, max) to simulate human-like typing
48
+ const TYPING_DELAY_MIN_MS = 30;
49
+ const TYPING_DELAY_MAX_MS = 100;
50
+ /**
51
+ * Type text character by character with random delays to simulate human typing.
52
+ *
53
+ * This triggers proper JavaScript input events that some websites require,
54
+ * unlike fill() which sets the value directly.
55
+ */
56
+ export async function typeLikeHuman(page, locator, text) {
57
+ await locator.click();
58
+ for (const character of text) {
59
+ await locator.pressSequentially(character);
60
+ const delay = Math.floor(Math.random() * (TYPING_DELAY_MAX_MS - TYPING_DELAY_MIN_MS + 1)) +
61
+ TYPING_DELAY_MIN_MS;
62
+ await page.waitForTimeout(delay);
63
+ }
64
+ }
65
+ // Script that creates the spinner overlay, designed to run in browser context
66
+ function createSpinnerOverlayScript(serviceName) {
67
+ return `
68
+ (() => {
69
+ if (document.getElementById('latchkey-spinner-overlay')) return;
70
+ const overlay = document.createElement('div');
71
+ overlay.id = 'latchkey-spinner-overlay';
72
+ overlay.innerHTML = \`
73
+ <style>
74
+ #latchkey-spinner-overlay {
75
+ position: fixed;
76
+ top: 0;
77
+ left: 0;
78
+ width: 100%;
79
+ height: 100%;
80
+ background: #f5f5f5;
81
+ display: flex;
82
+ flex-direction: column;
83
+ justify-content: center;
84
+ align-items: center;
85
+ z-index: 2147483647;
86
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
87
+ pointer-events: none;
88
+ }
89
+ #latchkey-spinner-overlay .spinner {
90
+ width: 50px;
91
+ height: 50px;
92
+ border: 4px solid #e0e0e0;
93
+ border-top-color: #007bff;
94
+ border-radius: 50%;
95
+ animation: latchkey-spin 1s linear infinite;
96
+ }
97
+ #latchkey-spinner-overlay .message {
98
+ margin-top: 20px;
99
+ color: #555;
100
+ font-size: 16px;
101
+ }
102
+ @keyframes latchkey-spin {
103
+ to { transform: rotate(360deg); }
104
+ }
105
+ </style>
106
+ <div class="spinner"></div>
107
+ <div class="message">Finalizing ${serviceName} login...</div>
108
+ \`;
109
+ document.body.appendChild(overlay);
110
+ })()
111
+ `;
112
+ }
113
+ /**
114
+ * Show a spinner overlay that hides page content from the user.
115
+ * The overlay persists across page navigations within the browser context.
116
+ */
117
+ export async function showSpinnerPage(context, serviceName) {
118
+ const spinnerPage = await context.newPage();
119
+ await spinnerPage.evaluate(createSpinnerOverlayScript(serviceName));
120
+ await spinnerPage.bringToFront();
121
+ }
122
+ //# sourceMappingURL=playwrightUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwrightUtils.js","sourceRoot":"","sources":["../../src/playwrightUtils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Service registry for looking up services by name or URL.
3
+ */
4
+ import { Service } from './services/index.js';
5
+ export declare class Registry {
6
+ readonly services: readonly Service[];
7
+ constructor(services: readonly Service[]);
8
+ getByName(name: string): Service | null;
9
+ getByUrl(url: string): Service | null;
10
+ }
11
+ export declare const REGISTRY: Registry;
12
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAA2C,MAAM,qBAAqB,CAAC;AAEvF,qBAAa,QAAQ;IACnB,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;gBAE1B,QAAQ,EAAE,SAAS,OAAO,EAAE;IAIxC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IASvC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;CAUtC;AAED,eAAO,MAAM,QAAQ,UAA0D,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Service registry for looking up services by name or URL.
3
+ */
4
+ import { SLACK, DISCORD, DROPBOX, GITHUB, LINEAR } from './services/index.js';
5
+ export class Registry {
6
+ services;
7
+ constructor(services) {
8
+ this.services = services;
9
+ }
10
+ getByName(name) {
11
+ for (const service of this.services) {
12
+ if (service.name === name) {
13
+ return service;
14
+ }
15
+ }
16
+ return null;
17
+ }
18
+ getByUrl(url) {
19
+ for (const service of this.services) {
20
+ for (const baseApiUrl of service.baseApiUrls) {
21
+ if (url.startsWith(baseApiUrl)) {
22
+ return service;
23
+ }
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ }
29
+ export const REGISTRY = new Registry([SLACK, DISCORD, DROPBOX, GITHUB, LINEAR]);
30
+ //# sourceMappingURL=registry.js.map