passwd-sso-cli 0.4.46 → 0.4.47

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.
@@ -7,7 +7,7 @@
7
7
  * Requires the vault to be unlocked (encryption key in memory) OR
8
8
  * an apiKey + PSSO_PASSPHRASE env var for non-interactive mode.
9
9
  */
10
- import { loadSecretsConfig, getPasswordPath } from "../lib/secrets-config.js";
10
+ import { loadSecretsConfig, getPasswordPath, getSecretsServerUrl } from "../lib/secrets-config.js";
11
11
  import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
12
12
  import { autoUnlockIfNeeded } from "./unlock.js";
13
13
  import { getToken } from "../lib/api-client.js";
@@ -22,10 +22,17 @@ export async function envCommand(opts) {
22
22
  }
23
23
  catch (err) {
24
24
  output.error(err instanceof Error ? err.message : "Failed to load config.");
25
- return;
25
+ process.exit(1);
26
26
  }
27
27
  const useV1 = !!config.apiKey;
28
- const baseUrl = config.server.replace(/\/$/, "");
28
+ let baseUrl;
29
+ try {
30
+ baseUrl = getSecretsServerUrl();
31
+ }
32
+ catch (err) {
33
+ output.error(err instanceof Error ? err.message : "Failed to resolve server URL.");
34
+ process.exit(1);
35
+ }
29
36
  // Determine auth header
30
37
  let authHeader;
31
38
  if (config.apiKey) {
@@ -35,14 +42,14 @@ export async function envCommand(opts) {
35
42
  const token = await getToken();
36
43
  if (!token) {
37
44
  output.error("Not logged in. Run `passwd-sso login` first, or set apiKey in config.");
38
- return;
45
+ process.exit(1);
39
46
  }
40
47
  authHeader = `Bearer ${token}`;
41
48
  }
42
49
  // Auto-unlock with PSSO_PASSPHRASE if needed
43
50
  if (!await autoUnlockIfNeeded()) {
44
51
  output.error("Vault is not unlocked. Run `passwd-sso unlock` first, or set PSSO_PASSPHRASE.");
45
- return;
52
+ process.exit(1);
46
53
  }
47
54
  const encryptionKey = getEncryptionKey();
48
55
  const userId = getUserId();
@@ -5,7 +5,7 @@
5
5
  * Blocks certain dangerous env var names from being overwritten.
6
6
  */
7
7
  import { spawn } from "node:child_process";
8
- import { loadSecretsConfig, getPasswordPath } from "../lib/secrets-config.js";
8
+ import { loadSecretsConfig, getPasswordPath, getSecretsServerUrl } from "../lib/secrets-config.js";
9
9
  import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
10
10
  import { autoUnlockIfNeeded } from "./unlock.js";
11
11
  import { getToken } from "../lib/api-client.js";
@@ -27,7 +27,14 @@ export async function runCommand(opts) {
27
27
  process.exit(1);
28
28
  }
29
29
  const useV1 = !!config.apiKey;
30
- const baseUrl = config.server.replace(/\/$/, "");
30
+ let baseUrl;
31
+ try {
32
+ baseUrl = getSecretsServerUrl();
33
+ }
34
+ catch (err) {
35
+ output.error(err instanceof Error ? err.message : "Failed to resolve server URL.");
36
+ process.exit(1);
37
+ }
31
38
  let authHeader;
32
39
  if (config.apiKey) {
33
40
  authHeader = `Bearer ${config.apiKey}`;
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Schema:
5
5
  * {
6
- * "server": "https://...",
7
6
  * "apiKey?": "api_...", // optional — uses /api/v1/ path
8
7
  * "secrets": {
9
8
  * "ENV_VAR_NAME": { "entry": "<entryId>", "field": "password" }
@@ -19,11 +18,11 @@ export interface SecretMapping {
19
18
  field: string;
20
19
  }
21
20
  export interface SecretsConfig {
22
- server: string;
23
21
  apiKey?: string;
24
22
  secrets: Record<string, SecretMapping>;
25
23
  }
26
24
  export declare function loadSecretsConfig(configPath?: string): SecretsConfig;
25
+ export declare function getSecretsServerUrl(): string;
27
26
  /**
28
27
  * Returns the API path for fetching a single password entry.
29
28
  * If apiKey is configured, use the public /api/v1/ path.
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Schema:
5
5
  * {
6
- * "server": "https://...",
7
6
  * "apiKey?": "api_...", // optional — uses /api/v1/ path
8
7
  * "secrets": {
9
8
  * "ENV_VAR_NAME": { "entry": "<entryId>", "field": "password" }
@@ -16,6 +15,11 @@
16
15
  */
17
16
  import { readFileSync, existsSync } from "node:fs";
18
17
  import { resolve } from "node:path";
18
+ import { loadConfig } from "./config.js";
19
+ import { validateServerUrl } from "./oauth.js";
20
+ function isPlaceholderEntryId(entryId) {
21
+ return entryId === "dummy-entry-id" || /^<[^>]+>$/.test(entryId);
22
+ }
19
23
  export function loadSecretsConfig(configPath) {
20
24
  const filePath = configPath
21
25
  ? resolve(configPath)
@@ -25,14 +29,41 @@ export function loadSecretsConfig(configPath) {
25
29
  }
26
30
  const raw = readFileSync(filePath, "utf-8");
27
31
  const parsed = JSON.parse(raw);
28
- if (!parsed.server || typeof parsed.server !== "string") {
29
- throw new Error("Config file must have a 'server' field.");
30
- }
31
32
  if (!parsed.secrets || typeof parsed.secrets !== "object") {
32
33
  throw new Error("Config file must have a 'secrets' field.");
33
34
  }
35
+ for (const [envName, mapping] of Object.entries(parsed.secrets)) {
36
+ if (!mapping || typeof mapping !== "object") {
37
+ throw new Error(`Secret mapping for '${envName}' must be an object.`);
38
+ }
39
+ const rawMapping = mapping;
40
+ if (typeof rawMapping.entry !== "string" || rawMapping.entry.trim().length === 0) {
41
+ throw new Error(`Secret mapping for '${envName}' must have a non-empty 'entry' string.`);
42
+ }
43
+ if (typeof rawMapping.field !== "string" || rawMapping.field.trim().length === 0) {
44
+ throw new Error(`Secret mapping for '${envName}' must have a non-empty 'field' string.`);
45
+ }
46
+ const entry = rawMapping.entry.trim();
47
+ const field = rawMapping.field.trim();
48
+ if (isPlaceholderEntryId(entry)) {
49
+ throw new Error(`Secret mapping for '${envName}' uses placeholder entry ID "${entry}". Replace it with a real vault entry ID.`);
50
+ }
51
+ // Preserve unknown keys (e.g. user-added comment fields) while normalising entry/field.
52
+ parsed.secrets[envName] = { ...rawMapping, entry, field };
53
+ }
34
54
  return parsed;
35
55
  }
56
+ export function getSecretsServerUrl() {
57
+ const { serverUrl } = loadConfig();
58
+ if (!serverUrl) {
59
+ throw new Error("Server URL not configured. Run `passwd-sso login -s <server-url>` once to configure it.");
60
+ }
61
+ // Defense-in-depth: re-validate the persisted URL before issuing a fetch
62
+ // with a Bearer token. Login validates at write time, but a hand-edited
63
+ // config file would otherwise reach fetch() unchecked.
64
+ validateServerUrl(serverUrl);
65
+ return serverUrl.replace(/\/$/, "");
66
+ }
36
67
  /**
37
68
  * Returns the API path for fetching a single password entry.
38
69
  * If apiKey is configured, use the public /api/v1/ path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passwd-sso-cli",
3
- "version": "0.4.46",
3
+ "version": "0.4.47",
4
4
  "description": "CLI for passwd-sso password manager",
5
5
  "type": "module",
6
6
  "license": "MIT",