passwd-sso-cli 0.4.3

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 (63) hide show
  1. package/dist/commands/agent-decrypt.d.ts +11 -0
  2. package/dist/commands/agent-decrypt.js +317 -0
  3. package/dist/commands/agent.d.ts +13 -0
  4. package/dist/commands/agent.js +116 -0
  5. package/dist/commands/api-key.d.ts +22 -0
  6. package/dist/commands/api-key.js +118 -0
  7. package/dist/commands/decrypt.d.ts +17 -0
  8. package/dist/commands/decrypt.js +108 -0
  9. package/dist/commands/env.d.ts +15 -0
  10. package/dist/commands/env.js +102 -0
  11. package/dist/commands/export.d.ts +7 -0
  12. package/dist/commands/export.js +99 -0
  13. package/dist/commands/generate.d.ts +11 -0
  14. package/dist/commands/generate.js +45 -0
  15. package/dist/commands/get.d.ts +8 -0
  16. package/dist/commands/get.js +73 -0
  17. package/dist/commands/list.d.ts +6 -0
  18. package/dist/commands/list.js +66 -0
  19. package/dist/commands/login.d.ts +4 -0
  20. package/dist/commands/login.js +45 -0
  21. package/dist/commands/run.d.ts +12 -0
  22. package/dist/commands/run.js +97 -0
  23. package/dist/commands/status.d.ts +6 -0
  24. package/dist/commands/status.js +62 -0
  25. package/dist/commands/totp.d.ts +7 -0
  26. package/dist/commands/totp.js +57 -0
  27. package/dist/commands/unlock.d.ts +19 -0
  28. package/dist/commands/unlock.js +125 -0
  29. package/dist/index.d.ts +8 -0
  30. package/dist/index.js +298 -0
  31. package/dist/lib/api-client.d.ts +22 -0
  32. package/dist/lib/api-client.js +145 -0
  33. package/dist/lib/blocked-keys.d.ts +2 -0
  34. package/dist/lib/blocked-keys.js +23 -0
  35. package/dist/lib/clipboard.d.ts +8 -0
  36. package/dist/lib/clipboard.js +79 -0
  37. package/dist/lib/config.d.ts +18 -0
  38. package/dist/lib/config.js +110 -0
  39. package/dist/lib/crypto-aad.d.ts +5 -0
  40. package/dist/lib/crypto-aad.js +44 -0
  41. package/dist/lib/crypto.d.ts +23 -0
  42. package/dist/lib/crypto.js +148 -0
  43. package/dist/lib/migrate.d.ts +8 -0
  44. package/dist/lib/migrate.js +87 -0
  45. package/dist/lib/openssh-key-parser.d.ts +17 -0
  46. package/dist/lib/openssh-key-parser.js +273 -0
  47. package/dist/lib/output.d.ts +10 -0
  48. package/dist/lib/output.js +36 -0
  49. package/dist/lib/paths.d.ts +17 -0
  50. package/dist/lib/paths.js +39 -0
  51. package/dist/lib/secrets-config.d.ts +31 -0
  52. package/dist/lib/secrets-config.js +48 -0
  53. package/dist/lib/ssh-agent-protocol.d.ts +56 -0
  54. package/dist/lib/ssh-agent-protocol.js +108 -0
  55. package/dist/lib/ssh-agent-socket.d.ts +20 -0
  56. package/dist/lib/ssh-agent-socket.js +187 -0
  57. package/dist/lib/ssh-key-agent.d.ts +54 -0
  58. package/dist/lib/ssh-key-agent.js +197 -0
  59. package/dist/lib/totp.d.ts +10 -0
  60. package/dist/lib/totp.js +31 -0
  61. package/dist/lib/vault-state.d.ts +15 -0
  62. package/dist/lib/vault-state.js +37 -0
  63. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * passwd-sso CLI — Password manager command-line interface.
4
+ *
5
+ * Long-lived process model: `unlock` enters a REPL loop where
6
+ * commands can be executed interactively. `lock` exits the process.
7
+ */
8
+ import { Command } from "commander";
9
+ import { createRequire } from "node:module";
10
+ import { createInterface } from "node:readline";
11
+ import { loginCommand } from "./commands/login.js";
12
+ import { statusCommand } from "./commands/status.js";
13
+ import { unlockCommand } from "./commands/unlock.js";
14
+ import { listCommand } from "./commands/list.js";
15
+ import { getCommand } from "./commands/get.js";
16
+ import { generateCommand } from "./commands/generate.js";
17
+ import { totpCommand } from "./commands/totp.js";
18
+ import { exportCommand } from "./commands/export.js";
19
+ import { envCommand } from "./commands/env.js";
20
+ import { runCommand } from "./commands/run.js";
21
+ import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRevokeCommand } from "./commands/api-key.js";
22
+ import { agentCommand } from "./commands/agent.js";
23
+ import { decryptCommand } from "./commands/decrypt.js";
24
+ import { isUnlocked, lockVault } from "./lib/vault-state.js";
25
+ import { clearPendingClipboard } from "./lib/clipboard.js";
26
+ import { setInsecure, clearTokenCache, startBackgroundRefresh, stopBackgroundRefresh } from "./lib/api-client.js";
27
+ import * as output from "./lib/output.js";
28
+ const require = createRequire(import.meta.url);
29
+ const cliPkg = require("../package.json");
30
+ const program = new Command();
31
+ program
32
+ .name("passwd-sso")
33
+ .description("Password manager CLI")
34
+ .version(cliPkg.version)
35
+ .option("-k, --insecure", "Allow self-signed TLS certificates")
36
+ .hook("preAction", () => {
37
+ if (program.opts().insecure) {
38
+ setInsecure(true);
39
+ }
40
+ });
41
+ program
42
+ .command("login")
43
+ .description("Configure server URL and authentication token")
44
+ .action(loginCommand);
45
+ program
46
+ .command("status")
47
+ .description("Show connection and vault status")
48
+ .option("--json", "Output as JSON")
49
+ .action((opts) => statusCommand({ json: opts.json }));
50
+ program
51
+ .command("unlock")
52
+ .description("Unlock the vault with your master passphrase")
53
+ .action(async () => {
54
+ await unlockCommand();
55
+ if (isUnlocked()) {
56
+ await interactiveMode();
57
+ }
58
+ });
59
+ program
60
+ .command("generate")
61
+ .description("Generate a secure password")
62
+ .option("-l, --length <n>", "Password length", "20")
63
+ .option("--no-uppercase", "Exclude uppercase letters")
64
+ .option("--no-digits", "Exclude digits")
65
+ .option("--no-symbols", "Exclude symbols")
66
+ .option("-c, --copy", "Copy to clipboard")
67
+ .option("--json", "Output as JSON")
68
+ .action((opts) => generateCommand({
69
+ length: parseInt(opts.length, 10),
70
+ noUppercase: opts.uppercase === false,
71
+ noDigits: opts.digits === false,
72
+ noSymbols: opts.symbols === false,
73
+ copy: opts.copy,
74
+ json: opts.json,
75
+ }));
76
+ program
77
+ .command("env")
78
+ .description("Output vault secrets as environment variables")
79
+ .option("-c, --config <path>", "Path to .passwd-sso-env.json")
80
+ .option("--format <format>", "Output format: shell, dotenv, json", "shell")
81
+ .action((opts) => envCommand({ config: opts.config, format: opts.format }));
82
+ program
83
+ .command("run")
84
+ .description("Inject vault secrets into a command's environment")
85
+ .option("-c, --config <path>", "Path to .passwd-sso-env.json")
86
+ .argument("<command...>", "Command to execute")
87
+ .action((command, opts) => runCommand({ config: opts.config, command }));
88
+ const apiKeyCmd = program
89
+ .command("api-key")
90
+ .description("Manage API keys");
91
+ apiKeyCmd
92
+ .command("list")
93
+ .description("List all API keys")
94
+ .option("--json", "Output as JSON")
95
+ .action((opts) => apiKeyListCommand({ json: opts.json }));
96
+ apiKeyCmd
97
+ .command("create")
98
+ .description("Create a new API key")
99
+ .requiredOption("-n, --name <name>", "Key name")
100
+ .option("-s, --scopes <scopes>", "Comma-separated scopes", "passwords:read")
101
+ .option("-d, --days <days>", "Expiry in days", "90")
102
+ .option("--json", "Output as JSON")
103
+ .action((opts) => apiKeyCreateCommand({
104
+ name: opts.name,
105
+ scopes: opts.scopes.split(","),
106
+ days: parseInt(opts.days, 10),
107
+ json: opts.json,
108
+ }));
109
+ apiKeyCmd
110
+ .command("revoke")
111
+ .description("Revoke an API key")
112
+ .argument("<id>", "API key ID")
113
+ .option("--json", "Output as JSON")
114
+ .action((id, opts) => apiKeyRevokeCommand(id, { json: opts.json }));
115
+ program
116
+ .command("agent")
117
+ .description("Start SSH agent or decrypt agent backed by vault")
118
+ .option("--eval", "Output shell eval-compatible commands")
119
+ .option("--decrypt", "Start decrypt agent mode (Unix socket)")
120
+ .action((opts) => agentCommand({ eval: opts.eval, decrypt: opts.decrypt }));
121
+ program
122
+ .command("decrypt <id>")
123
+ .description("Decrypt a credential field via agent socket (for hooks)")
124
+ .requiredOption("--mcp-client <clientId>", "MCP client ID (mcpc_xxx) for authorization")
125
+ .option("--field <field>", "Field to decrypt (default: password, ignored with --json)")
126
+ .option("--json", "Output all decrypted fields as JSON (ignores --field)")
127
+ .action((id, opts) => decryptCommand(id, { field: opts.field, mcpClient: opts.mcpClient, json: opts.json }));
128
+ program.parse();
129
+ // ─── Interactive REPL Mode ──────────────────────────────────────
130
+ async function interactiveMode() {
131
+ output.info("Vault unlocked. Type a command or `help` for options. `lock` to exit.");
132
+ // Keep token alive during REPL session
133
+ startBackgroundRefresh();
134
+ const rl = createInterface({
135
+ input: process.stdin,
136
+ output: process.stdout,
137
+ prompt: "passwd-sso> ",
138
+ });
139
+ rl.prompt();
140
+ for await (const line of rl) {
141
+ const args = line.trim().split(/\s+/);
142
+ const cmd = args[0];
143
+ if (!cmd) {
144
+ rl.prompt();
145
+ continue;
146
+ }
147
+ try {
148
+ switch (cmd) {
149
+ case "list":
150
+ case "ls":
151
+ await listCommand({ json: args.includes("--json") });
152
+ break;
153
+ case "get":
154
+ if (!args[1]) {
155
+ output.error("Usage: get <id> [--copy] [--json] [--field <name>]");
156
+ }
157
+ else {
158
+ const fieldIdx = args.indexOf("--field");
159
+ const fieldArg = fieldIdx !== -1 ? args[fieldIdx + 1] : undefined;
160
+ const field = fieldArg && !fieldArg.startsWith("-") ? fieldArg : undefined;
161
+ if (fieldIdx !== -1 && !field) {
162
+ output.error("Usage: get <id> --field <name>");
163
+ break;
164
+ }
165
+ await getCommand(args[1], {
166
+ copy: args.includes("--copy") || args.includes("-c"),
167
+ json: args.includes("--json"),
168
+ field,
169
+ });
170
+ }
171
+ break;
172
+ case "totp":
173
+ if (!args[1]) {
174
+ output.error("Usage: totp <id> [--copy] [--json]");
175
+ }
176
+ else {
177
+ await totpCommand(args[1], {
178
+ copy: args.includes("--copy") || args.includes("-c"),
179
+ json: args.includes("--json"),
180
+ });
181
+ }
182
+ break;
183
+ case "generate":
184
+ case "gen": {
185
+ const lengthIdx = args.indexOf("-l");
186
+ const lengthIdx2 = args.indexOf("--length");
187
+ const idx = lengthIdx !== -1 ? lengthIdx : lengthIdx2;
188
+ const rawLength = idx !== -1 ? args[idx + 1] : undefined;
189
+ const parsedLength = rawLength && !rawLength.startsWith("-") ? parseInt(rawLength, 10) : NaN;
190
+ if (idx !== -1 && (isNaN(parsedLength) || parsedLength <= 0)) {
191
+ output.error("Usage: generate [-l <number>] [--copy]");
192
+ break;
193
+ }
194
+ await generateCommand({
195
+ length: isNaN(parsedLength) ? 20 : parsedLength,
196
+ copy: args.includes("--copy") || args.includes("-c"),
197
+ json: args.includes("--json"),
198
+ });
199
+ break;
200
+ }
201
+ case "export": {
202
+ const fmtIdx = args.indexOf("--format");
203
+ const outLong = args.indexOf("--output");
204
+ const outShort = args.indexOf("-o");
205
+ const outIdx = outLong !== -1 ? outLong : outShort;
206
+ await exportCommand({
207
+ format: fmtIdx !== -1 ? args[fmtIdx + 1] : "json",
208
+ output: outIdx !== -1 ? args[outIdx + 1] : undefined,
209
+ });
210
+ break;
211
+ }
212
+ case "env": {
213
+ const envFmtIdx = args.indexOf("--format");
214
+ const envConfIdx = args.indexOf("-c");
215
+ const envConfIdx2 = args.indexOf("--config");
216
+ const envCIdx = envConfIdx !== -1 ? envConfIdx : envConfIdx2;
217
+ await envCommand({
218
+ config: envCIdx !== -1 ? args[envCIdx + 1] : undefined,
219
+ format: envFmtIdx !== -1 ? args[envFmtIdx + 1] : "shell",
220
+ });
221
+ break;
222
+ }
223
+ case "decrypt": {
224
+ if (!args[1]) {
225
+ output.error("Usage: decrypt <id> --mcp-client <mcpc_xxx> [--field <field>]");
226
+ }
227
+ else {
228
+ const tokenIdx = args.indexOf("--mcp-client");
229
+ const mcpClient = tokenIdx !== -1 ? args[tokenIdx + 1] : undefined;
230
+ if (!mcpClient || mcpClient.startsWith("-")) {
231
+ output.error("Usage: decrypt <id> --mcp-client <mcpc_xxx> [--field <field>]");
232
+ break;
233
+ }
234
+ const fieldIdx = args.indexOf("--field");
235
+ const fieldArg = fieldIdx !== -1 ? args[fieldIdx + 1] : undefined;
236
+ const field = fieldArg && !fieldArg.startsWith("-") ? fieldArg : undefined;
237
+ const json = args.includes("--json");
238
+ await decryptCommand(args[1], { field, mcpClient, json });
239
+ }
240
+ break;
241
+ }
242
+ case "api-key": {
243
+ const sub = args[1];
244
+ if (sub === "list") {
245
+ await apiKeyListCommand({ json: args.includes("--json") });
246
+ }
247
+ else {
248
+ output.error("Usage: api-key list [--json]");
249
+ }
250
+ break;
251
+ }
252
+ case "status":
253
+ await statusCommand({ json: args.includes("--json") });
254
+ break;
255
+ case "lock":
256
+ case "exit":
257
+ case "quit":
258
+ stopBackgroundRefresh();
259
+ lockVault();
260
+ clearTokenCache();
261
+ clearPendingClipboard();
262
+ output.success("Vault locked.");
263
+ rl.close();
264
+ return;
265
+ case "help":
266
+ case "?":
267
+ console.log(`
268
+ Commands:
269
+ list [--json] List all entries
270
+ get <id> [--copy] [--json] Show entry details
271
+ get <id> --field password --copy Copy a specific field
272
+ totp <id> [--copy] [--json] Generate TOTP code
273
+ generate [-l N] [--copy] [--json] Generate password
274
+ export [--format json|csv] [-o file] Export vault
275
+ env [--format shell|dotenv|json] Output vault secrets as env vars
276
+ decrypt <id> --mcp-client <mcpc_xxx> [--field] Decrypt via agent socket
277
+ api-key list [--json] List API keys
278
+ status [--json] Show connection status
279
+ lock Lock vault and exit
280
+ `.trim());
281
+ break;
282
+ default:
283
+ output.error(`Unknown command: ${cmd}. Type 'help' for options.`);
284
+ }
285
+ }
286
+ catch (err) {
287
+ output.error(err instanceof Error ? err.message : "An error occurred");
288
+ }
289
+ rl.prompt();
290
+ }
291
+ // EOF (Ctrl+D) — cleanup
292
+ stopBackgroundRefresh();
293
+ lockVault();
294
+ clearTokenCache();
295
+ clearPendingClipboard();
296
+ output.success("Vault locked.");
297
+ }
298
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * API client for the CLI tool.
3
+ *
4
+ * Uses native Node.js fetch with Bearer token authentication.
5
+ * Automatically refreshes expired tokens.
6
+ */
7
+ export declare function setInsecure(enabled: boolean): void;
8
+ export declare function getToken(): Promise<string | null>;
9
+ export declare function setTokenCache(token: string, expiresAt?: string): void;
10
+ export declare function clearTokenCache(): void;
11
+ export interface ApiResponse<T = unknown> {
12
+ ok: boolean;
13
+ status: number;
14
+ data: T;
15
+ }
16
+ export declare function apiRequest<T = unknown>(path: string, options?: {
17
+ method?: string;
18
+ body?: unknown;
19
+ headers?: Record<string, string>;
20
+ }): Promise<ApiResponse<T>>;
21
+ export declare function startBackgroundRefresh(): void;
22
+ export declare function stopBackgroundRefresh(): void;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * API client for the CLI tool.
3
+ *
4
+ * Uses native Node.js fetch with Bearer token authentication.
5
+ * Automatically refreshes expired tokens.
6
+ */
7
+ import { loadToken, saveToken, loadConfig, saveConfig } from "./config.js";
8
+ let cachedToken = null;
9
+ let cachedExpiresAt = null;
10
+ export function setInsecure(enabled) {
11
+ if (enabled) {
12
+ process.stderr.write("WARNING: TLS certificate verification is disabled. Your credentials may be intercepted.\n");
13
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
14
+ }
15
+ }
16
+ export async function getToken() {
17
+ if (cachedToken)
18
+ return cachedToken;
19
+ cachedToken = await loadToken();
20
+ return cachedToken;
21
+ }
22
+ export function setTokenCache(token, expiresAt) {
23
+ cachedToken = token;
24
+ if (expiresAt) {
25
+ cachedExpiresAt = new Date(expiresAt).getTime();
26
+ }
27
+ }
28
+ export function clearTokenCache() {
29
+ cachedToken = null;
30
+ cachedExpiresAt = null;
31
+ }
32
+ function getBaseUrl() {
33
+ const config = loadConfig();
34
+ if (!config.serverUrl) {
35
+ throw new Error("Server URL not configured. Run `passwd-sso login` first.");
36
+ }
37
+ return config.serverUrl.replace(/\/$/, "");
38
+ }
39
+ /** Refresh buffer: refresh 2 minutes before expiry */
40
+ const REFRESH_BUFFER_MS = 2 * 60 * 1000;
41
+ function isTokenExpiringSoon() {
42
+ if (!cachedExpiresAt) {
43
+ // Load from config if not cached
44
+ const config = loadConfig();
45
+ if (config.tokenExpiresAt) {
46
+ cachedExpiresAt = new Date(config.tokenExpiresAt).getTime();
47
+ }
48
+ }
49
+ if (!cachedExpiresAt)
50
+ return false;
51
+ return Date.now() >= cachedExpiresAt - REFRESH_BUFFER_MS;
52
+ }
53
+ async function refreshToken() {
54
+ const token = await getToken();
55
+ if (!token)
56
+ return false;
57
+ const baseUrl = getBaseUrl();
58
+ try {
59
+ const res = await fetch(`${baseUrl}/api/extension/token/refresh`, {
60
+ method: "POST",
61
+ headers: {
62
+ Authorization: `Bearer ${token}`,
63
+ "Content-Type": "application/json",
64
+ },
65
+ });
66
+ if (!res.ok)
67
+ return false;
68
+ const data = (await res.json());
69
+ if (typeof data.token !== "string" || !data.token)
70
+ return false;
71
+ if (typeof data.expiresAt !== "string" || isNaN(new Date(data.expiresAt).getTime()))
72
+ return false;
73
+ await saveToken(data.token);
74
+ setTokenCache(data.token, data.expiresAt);
75
+ // Persist expiresAt in config
76
+ const config = loadConfig();
77
+ config.tokenExpiresAt = data.expiresAt;
78
+ saveConfig(config);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ export async function apiRequest(path, options = {}) {
86
+ let token = await getToken();
87
+ if (!token) {
88
+ throw new Error("Not logged in. Run `passwd-sso login` first.");
89
+ }
90
+ // Proactively refresh if token is expiring soon
91
+ if (isTokenExpiringSoon()) {
92
+ const refreshed = await refreshToken();
93
+ if (refreshed) {
94
+ token = await getToken();
95
+ }
96
+ }
97
+ const baseUrl = getBaseUrl();
98
+ const url = `${baseUrl}${path}`;
99
+ const { method = "GET", body, headers = {} } = options;
100
+ const fetchOpts = {
101
+ method,
102
+ headers: {
103
+ Authorization: `Bearer ${token}`,
104
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
105
+ ...headers,
106
+ },
107
+ };
108
+ if (body !== undefined) {
109
+ fetchOpts.body = JSON.stringify(body);
110
+ }
111
+ let res = await fetch(url, fetchOpts);
112
+ // Auto-refresh on 401
113
+ if (res.status === 401) {
114
+ const refreshed = await refreshToken();
115
+ if (refreshed) {
116
+ const newToken = await getToken();
117
+ fetchOpts.headers = {
118
+ ...fetchOpts.headers,
119
+ Authorization: `Bearer ${newToken}`,
120
+ };
121
+ res = await fetch(url, fetchOpts);
122
+ }
123
+ }
124
+ const data = (await res.json().catch(() => ({})));
125
+ return { ok: res.ok, status: res.status, data };
126
+ }
127
+ // ─── Background Token Refresh Timer ─────────────────────────
128
+ /** Interval: refresh every 10 minutes (well within 15-min TTL) */
129
+ const BG_REFRESH_INTERVAL_MS = 10 * 60 * 1000;
130
+ let bgRefreshTimer = null;
131
+ export function startBackgroundRefresh() {
132
+ stopBackgroundRefresh();
133
+ bgRefreshTimer = setInterval(() => {
134
+ void refreshToken();
135
+ }, BG_REFRESH_INTERVAL_MS);
136
+ // Don't keep the process alive just for the timer
137
+ bgRefreshTimer.unref();
138
+ }
139
+ export function stopBackgroundRefresh() {
140
+ if (bgRefreshTimer) {
141
+ clearInterval(bgRefreshTimer);
142
+ bgRefreshTimer = null;
143
+ }
144
+ }
145
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1,2 @@
1
+ /** Env vars that must never be overwritten (case-insensitive). */
2
+ export declare const BLOCKED_KEYS: Set<string>;
@@ -0,0 +1,23 @@
1
+ /** Env vars that must never be overwritten (case-insensitive). */
2
+ export const BLOCKED_KEYS = new Set([
3
+ "PATH",
4
+ "LD_PRELOAD",
5
+ "LD_LIBRARY_PATH",
6
+ "DYLD_INSERT_LIBRARIES",
7
+ "DYLD_FRAMEWORK_PATH",
8
+ "NODE_OPTIONS",
9
+ "NODE_PATH",
10
+ "PYTHONPATH",
11
+ "PYTHONSTARTUP",
12
+ "PYTHONUSERBASE",
13
+ "RUBYLIB",
14
+ "RUBYOPT",
15
+ "PERL5LIB",
16
+ "PERL5OPT",
17
+ "JAVA_TOOL_OPTIONS",
18
+ "_JAVA_OPTIONS",
19
+ "CLASSPATH",
20
+ "BASH_ENV",
21
+ "ENV",
22
+ ]);
23
+ //# sourceMappingURL=blocked-keys.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Clipboard utilities with auto-clear after 30 seconds.
3
+ *
4
+ * Uses clipboardy for cross-platform support.
5
+ * Compares clipboard hash before clearing to avoid overwriting user's copy.
6
+ */
7
+ export declare function copyToClipboard(content: string): Promise<void>;
8
+ export declare function clearPendingClipboard(): void;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Clipboard utilities with auto-clear after 30 seconds.
3
+ *
4
+ * Uses clipboardy for cross-platform support.
5
+ * Compares clipboard hash before clearing to avoid overwriting user's copy.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { execSync } from "node:child_process";
9
+ const CLEAR_TIMEOUT_MS = 30_000;
10
+ let clearTimer = null;
11
+ let copiedHash = null;
12
+ function hashContent(content) {
13
+ return createHash("sha256").update(content).digest("hex");
14
+ }
15
+ async function getClipboardy() {
16
+ return import("clipboardy");
17
+ }
18
+ export async function copyToClipboard(content) {
19
+ const { default: clipboardy } = await getClipboardy();
20
+ await clipboardy.write(content);
21
+ copiedHash = hashContent(content);
22
+ // Cancel previous timer
23
+ if (clearTimer) {
24
+ clearTimeout(clearTimer);
25
+ clearTimer = null;
26
+ }
27
+ // Schedule auto-clear
28
+ const timer = setTimeout(async () => {
29
+ try {
30
+ const current = await clipboardy.read();
31
+ if (copiedHash && hashContent(current) === copiedHash) {
32
+ await clipboardy.write("");
33
+ }
34
+ }
35
+ catch {
36
+ // ignore clipboard read errors
37
+ }
38
+ copiedHash = null;
39
+ clearTimer = null;
40
+ }, CLEAR_TIMEOUT_MS);
41
+ timer.unref();
42
+ clearTimer = timer;
43
+ }
44
+ export function clearPendingClipboard() {
45
+ if (clearTimer) {
46
+ clearTimeout(clearTimer);
47
+ clearTimer = null;
48
+ }
49
+ copiedHash = null;
50
+ }
51
+ // Signal handlers to clear clipboard on exit
52
+ function onExit() {
53
+ if (copiedHash) {
54
+ // Best-effort synchronous clipboard clear — cannot await in exit handler.
55
+ // Use platform-specific commands to actually clear the clipboard content.
56
+ try {
57
+ if (process.platform === "darwin") {
58
+ execSync("pbcopy < /dev/null", { stdio: "ignore", timeout: 1000 });
59
+ }
60
+ else if (process.platform === "linux") {
61
+ execSync("xclip -selection clipboard < /dev/null 2>/dev/null || xsel --clipboard --delete 2>/dev/null", { stdio: "ignore", timeout: 1000, shell: "/bin/sh" });
62
+ }
63
+ }
64
+ catch {
65
+ // Ignore — best effort only
66
+ }
67
+ clearPendingClipboard();
68
+ }
69
+ }
70
+ process.on("SIGINT", () => {
71
+ onExit();
72
+ process.exit(130);
73
+ });
74
+ process.on("SIGTERM", () => {
75
+ onExit();
76
+ process.exit(143);
77
+ });
78
+ process.on("exit", onExit);
79
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Configuration management for the CLI tool.
3
+ *
4
+ * Config file: $XDG_CONFIG_HOME/passwd-sso/config.json
5
+ * Credentials: OS keychain (via keytar) or $XDG_DATA_HOME/passwd-sso/credentials
6
+ *
7
+ * Legacy ~/.passwd-sso/ is auto-migrated on first access.
8
+ */
9
+ export interface CliConfig {
10
+ serverUrl: string;
11
+ locale: string;
12
+ tokenExpiresAt?: string;
13
+ }
14
+ export declare function loadConfig(): CliConfig;
15
+ export declare function saveConfig(config: CliConfig): void;
16
+ export declare function saveToken(token: string): Promise<"keychain" | "file">;
17
+ export declare function loadToken(): Promise<string | null>;
18
+ export declare function deleteToken(): Promise<void>;