Skip to content

Commit 96ae80e

Browse files
committed
feat(1password): add SDK integration and global type definitions
Add 1Password SDK-based secret resolution alongside CLI support: - New onepassword module using @1password/sdk - SDK initialization with service account token auto-discovery - Secret caching with configurable TTL - Interactive configuration wizard - Fallback to CLI methods when SDK unavailable Also add global type definitions for screen recording and measure APIs.
1 parent f155fa9 commit 96ae80e

3 files changed

Lines changed: 682 additions & 14 deletions

File tree

src/api/onepassword.ts

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import { createClient, Client } from "@1password/sdk";
2+
import { readFile, writeFile } from "node:fs/promises";
3+
import { existsSync } from "node:fs";
4+
import { homedir } from "node:os";
5+
import { join } from "node:path";
6+
7+
let opClient: Client | null = null;
8+
let opInitPromise: Promise<Client | null> | null = null;
9+
10+
/**
11+
* Global cache for resolved secrets to minimize API calls
12+
*/
13+
const secretCache = new Map<string, { value: string; timestamp: number }>();
14+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes default
15+
16+
/**
17+
* Configuration for 1Password SDK
18+
*/
19+
interface OnePasswordConfig {
20+
serviceAccountToken?: string;
21+
vault?: string;
22+
cacheTTL?: number;
23+
integrationName?: string;
24+
integrationVersion?: string;
25+
}
26+
27+
let config: OnePasswordConfig = {
28+
integrationName: "Script Kit",
29+
integrationVersion: "1.0.0",
30+
cacheTTL: CACHE_TTL,
31+
};
32+
33+
/**
34+
* Initialize the 1Password client
35+
*/
36+
const initOnePassword = async (): Promise<Client | null> => {
37+
// Return existing client if already initialized
38+
if (opClient) return opClient;
39+
40+
// If initialization is in progress, wait for it
41+
if (opInitPromise) return opInitPromise;
42+
43+
// Start initialization
44+
opInitPromise = (async () => {
45+
try {
46+
// Try to get service account token from various sources
47+
let token = config.serviceAccountToken;
48+
49+
if (!token) {
50+
token = await getServiceAccountToken();
51+
}
52+
53+
if (!token) {
54+
log(`No 1Password service account token found. Please set OP_SERVICE_ACCOUNT_TOKEN or run: await onepassword.configure()`);
55+
return null;
56+
}
57+
58+
opClient = await createClient({
59+
auth: token,
60+
integrationName: config.integrationName || "Script Kit",
61+
integrationVersion: config.integrationVersion || "1.0.0",
62+
});
63+
64+
log(`✓ 1Password SDK initialized`);
65+
return opClient;
66+
} catch (error) {
67+
log(`Failed to initialize 1Password SDK: ${error}`);
68+
return null;
69+
} finally {
70+
opInitPromise = null;
71+
}
72+
})();
73+
74+
return opInitPromise;
75+
};
76+
77+
/**
78+
* Get service account token from various sources
79+
*/
80+
const getServiceAccountToken = async (): Promise<string | null> => {
81+
// 1. Check environment variable
82+
if (process.env.OP_SERVICE_ACCOUNT_TOKEN) {
83+
return process.env.OP_SERVICE_ACCOUNT_TOKEN;
84+
}
85+
86+
// 2. Check .env file in kenv
87+
const kenvEnvPath = kenvPath(".env");
88+
if (existsSync(kenvEnvPath)) {
89+
try {
90+
const envContent = await readFile(kenvEnvPath, "utf-8");
91+
const match = envContent.match(/OP_SERVICE_ACCOUNT_TOKEN=(.+)/);
92+
if (match?.[1]) {
93+
return match[1].trim();
94+
}
95+
} catch (error) {
96+
// Ignore read errors
97+
}
98+
}
99+
100+
// 3. Check user's home directory config
101+
const configPath = join(homedir(), ".config", "op", "service-account.json");
102+
if (existsSync(configPath)) {
103+
try {
104+
const configContent = await readFile(configPath, "utf-8");
105+
const configData = JSON.parse(configContent);
106+
if (configData.token) {
107+
return configData.token;
108+
}
109+
} catch (error) {
110+
// Ignore parse errors
111+
}
112+
}
113+
114+
// 4. Check Script Kit's app data
115+
const appDataPath = kenvPath("..", "..", "app-data", "1password.json");
116+
if (existsSync(appDataPath)) {
117+
try {
118+
const appData = await readFile(appDataPath, "utf-8");
119+
const data = JSON.parse(appData);
120+
if (data.serviceAccountToken) {
121+
return data.serviceAccountToken;
122+
}
123+
} catch (error) {
124+
// Ignore parse errors
125+
}
126+
}
127+
128+
return null;
129+
};
130+
131+
/**
132+
* Resolve a 1Password secret reference
133+
*/
134+
const resolveSecret = async (
135+
reference: string,
136+
defaultValue?: string
137+
): Promise<string> => {
138+
// Check cache first
139+
const cached = secretCache.get(reference);
140+
if (cached && Date.now() - cached.timestamp < (config.cacheTTL || CACHE_TTL)) {
141+
return cached.value;
142+
}
143+
144+
// Initialize client if needed
145+
const client = await initOnePassword();
146+
if (!client) {
147+
if (defaultValue !== undefined) {
148+
return defaultValue;
149+
}
150+
// Fall back to prompting user if no default provided
151+
return await mini({
152+
placeholder: `Enter value for ${reference}:`,
153+
secret: true,
154+
});
155+
}
156+
157+
try {
158+
// Resolve the secret using the SDK
159+
const value = await client.secrets.resolve(reference);
160+
161+
// Cache the result
162+
secretCache.set(reference, { value, timestamp: Date.now() });
163+
164+
return value;
165+
} catch (error) {
166+
log(`Failed to resolve secret ${reference}: ${error}`);
167+
168+
if (defaultValue !== undefined) {
169+
return defaultValue;
170+
}
171+
172+
// Fall back to prompting user
173+
return await mini({
174+
placeholder: `Enter value for ${reference}:`,
175+
secret: true,
176+
});
177+
}
178+
};
179+
180+
/**
181+
* Get an item from 1Password
182+
*/
183+
const getItem = async (
184+
vault: string,
185+
item: string
186+
): Promise<any | null> => {
187+
const client = await initOnePassword();
188+
if (!client) {
189+
log(`1Password SDK not initialized`);
190+
return null;
191+
}
192+
193+
try {
194+
// Note: The SDK v0.3.1 has limited item operations
195+
// This is a placeholder for when more operations are available
196+
const reference = `op://${vault}/${item}`;
197+
const value = await client.secrets.resolve(reference);
198+
return value;
199+
} catch (error) {
200+
log(`Failed to get item ${item} from vault ${vault}: ${error}`);
201+
return null;
202+
}
203+
};
204+
205+
/**
206+
* Configure 1Password integration
207+
*/
208+
const configure = async (options?: OnePasswordConfig): Promise<void> => {
209+
if (options) {
210+
config = { ...config, ...options };
211+
}
212+
213+
// Interactive configuration if no token provided
214+
if (!config.serviceAccountToken) {
215+
const token = await mini({
216+
placeholder: "Enter your 1Password Service Account Token:",
217+
secret: true,
218+
hint: "Get a token from: https://my.1password.com/integrations/directory/scriptkit",
219+
});
220+
221+
if (token) {
222+
config.serviceAccountToken = token;
223+
224+
// Offer to save the token
225+
const saveLocation = await select("Where would you like to save the token?", [
226+
{ name: "Script Kit .env (recommended)", value: "kenv" },
227+
{ name: "System environment", value: "system" },
228+
{ name: "Don't save", value: "none" },
229+
]);
230+
231+
if (saveLocation === "kenv") {
232+
const envPath = kenvPath(".env");
233+
let envContent = "";
234+
235+
if (existsSync(envPath)) {
236+
envContent = await readFile(envPath, "utf-8");
237+
}
238+
239+
// Add or update the token
240+
if (envContent.includes("OP_SERVICE_ACCOUNT_TOKEN=")) {
241+
envContent = envContent.replace(
242+
/OP_SERVICE_ACCOUNT_TOKEN=.*/,
243+
`OP_SERVICE_ACCOUNT_TOKEN=${token}`
244+
);
245+
} else {
246+
envContent += `\n# 1Password Service Account Token\nOP_SERVICE_ACCOUNT_TOKEN=${token}\n`;
247+
}
248+
249+
await writeFile(envPath, envContent);
250+
log(`✓ Token saved to ${envPath}`);
251+
}
252+
}
253+
}
254+
255+
// Test the configuration
256+
const client = await initOnePassword();
257+
if (client) {
258+
notify("✓ 1Password SDK configured successfully");
259+
} else {
260+
notify("❌ Failed to configure 1Password SDK");
261+
}
262+
};
263+
264+
/**
265+
* Clear the secret cache
266+
*/
267+
const clearCache = (): void => {
268+
secretCache.clear();
269+
log("1Password secret cache cleared");
270+
};
271+
272+
/**
273+
* List available vaults (requires CLI for now)
274+
*/
275+
const listVaults = async (): Promise<string[]> => {
276+
try {
277+
const { stdout } = await exec("op vault list --format=json");
278+
const vaults = JSON.parse(stdout);
279+
return vaults.map((v: any) => v.name);
280+
} catch (error) {
281+
log(`Failed to list vaults: ${error}`);
282+
log(`Note: This operation requires 1Password CLI to be installed`);
283+
return [];
284+
}
285+
};
286+
287+
/**
288+
* Create a 1Password reference from components
289+
*/
290+
const createReference = (
291+
vault: string,
292+
item: string,
293+
field?: string,
294+
section?: string
295+
): string => {
296+
let ref = `op://${vault}/${item}`;
297+
298+
if (section) {
299+
ref += `/${section}`;
300+
}
301+
302+
if (field) {
303+
ref += `/${field}`;
304+
}
305+
306+
return ref;
307+
};
308+
309+
/**
310+
* Main onepassword helper with sub-methods
311+
*/
312+
export const onepassword = async (
313+
reference: string,
314+
defaultValue?: string
315+
): Promise<string> => {
316+
return resolveSecret(reference, defaultValue);
317+
};
318+
319+
// Add utility methods to the function
320+
onepassword.resolve = resolveSecret;
321+
onepassword.configure = configure;
322+
onepassword.getItem = getItem;
323+
onepassword.clearCache = clearCache;
324+
onepassword.listVaults = listVaults;
325+
onepassword.createReference = createReference;
326+
327+
// Export configuration interface
328+
onepassword.config = config;
329+
330+
// Add initialization method
331+
onepassword.init = initOnePassword;
332+
333+
// Export for use in other modules
334+
export default onepassword;
335+
336+
// Also make it available globally
337+
global.onepassword = onepassword;

0 commit comments

Comments
 (0)