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.
- package/dist/commands/agent-decrypt.d.ts +11 -0
- package/dist/commands/agent-decrypt.js +317 -0
- package/dist/commands/agent.d.ts +13 -0
- package/dist/commands/agent.js +116 -0
- package/dist/commands/api-key.d.ts +22 -0
- package/dist/commands/api-key.js +118 -0
- package/dist/commands/decrypt.d.ts +17 -0
- package/dist/commands/decrypt.js +108 -0
- package/dist/commands/env.d.ts +15 -0
- package/dist/commands/env.js +102 -0
- package/dist/commands/export.d.ts +7 -0
- package/dist/commands/export.js +99 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/generate.js +45 -0
- package/dist/commands/get.d.ts +8 -0
- package/dist/commands/get.js +73 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +66 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +45 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +97 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +62 -0
- package/dist/commands/totp.d.ts +7 -0
- package/dist/commands/totp.js +57 -0
- package/dist/commands/unlock.d.ts +19 -0
- package/dist/commands/unlock.js +125 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +298 -0
- package/dist/lib/api-client.d.ts +22 -0
- package/dist/lib/api-client.js +145 -0
- package/dist/lib/blocked-keys.d.ts +2 -0
- package/dist/lib/blocked-keys.js +23 -0
- package/dist/lib/clipboard.d.ts +8 -0
- package/dist/lib/clipboard.js +79 -0
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/crypto-aad.d.ts +5 -0
- package/dist/lib/crypto-aad.js +44 -0
- package/dist/lib/crypto.d.ts +23 -0
- package/dist/lib/crypto.js +148 -0
- package/dist/lib/migrate.d.ts +8 -0
- package/dist/lib/migrate.js +87 -0
- package/dist/lib/openssh-key-parser.d.ts +17 -0
- package/dist/lib/openssh-key-parser.js +273 -0
- package/dist/lib/output.d.ts +10 -0
- package/dist/lib/output.js +36 -0
- package/dist/lib/paths.d.ts +17 -0
- package/dist/lib/paths.js +39 -0
- package/dist/lib/secrets-config.d.ts +31 -0
- package/dist/lib/secrets-config.js +48 -0
- package/dist/lib/ssh-agent-protocol.d.ts +56 -0
- package/dist/lib/ssh-agent-protocol.js +108 -0
- package/dist/lib/ssh-agent-socket.d.ts +20 -0
- package/dist/lib/ssh-agent-socket.js +187 -0
- package/dist/lib/ssh-key-agent.d.ts +54 -0
- package/dist/lib/ssh-key-agent.js +197 -0
- package/dist/lib/totp.d.ts +10 -0
- package/dist/lib/totp.js +31 -0
- package/dist/lib/vault-state.d.ts +15 -0
- package/dist/lib/vault-state.js +37 -0
- 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,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>;
|