passwd-sso-cli 0.4.46 → 0.4.48
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/api-key.js +6 -5
- package/dist/commands/env.js +12 -5
- package/dist/commands/run.js +9 -2
- package/dist/lib/api-error-body.d.ts +55 -0
- package/dist/lib/api-error-body.js +90 -0
- package/dist/lib/secrets-config.d.ts +1 -2
- package/dist/lib/secrets-config.js +35 -4
- package/package.json +1 -1
package/dist/commands/api-key.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* revoke — Revoke an API key
|
|
8
8
|
*/
|
|
9
9
|
import { apiRequest } from "../lib/api-client.js";
|
|
10
|
+
import { readMainApiErrorBody } from "../lib/api-error-body.js";
|
|
10
11
|
import * as output from "../lib/output.js";
|
|
11
12
|
export async function apiKeyListCommand(options = {}) {
|
|
12
13
|
const res = await apiRequest("/api/api-keys");
|
|
@@ -73,8 +74,8 @@ export async function apiKeyCreateCommand(opts) {
|
|
|
73
74
|
},
|
|
74
75
|
});
|
|
75
76
|
if (!res.ok) {
|
|
76
|
-
const err = res.data;
|
|
77
|
-
output.error(`Failed to create API key: ${err
|
|
77
|
+
const err = readMainApiErrorBody(res.data);
|
|
78
|
+
output.error(`Failed to create API key: ${err?.error ?? `HTTP ${res.status}`}`);
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
if (opts.json) {
|
|
@@ -99,12 +100,12 @@ export async function apiKeyCreateCommand(opts) {
|
|
|
99
100
|
export async function apiKeyRevokeCommand(id, options = {}) {
|
|
100
101
|
const res = await apiRequest(`/api/api-keys/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
101
102
|
if (!res.ok) {
|
|
102
|
-
const err = res.data;
|
|
103
|
+
const err = readMainApiErrorBody(res.data);
|
|
103
104
|
if (options.json) {
|
|
104
|
-
output.json({ success: false, error: err
|
|
105
|
+
output.json({ success: false, error: err?.error ?? `HTTP ${res.status}` });
|
|
105
106
|
}
|
|
106
107
|
else {
|
|
107
|
-
output.error(`Failed to revoke API key: ${err
|
|
108
|
+
output.error(`Failed to revoke API key: ${err?.error ?? `HTTP ${res.status}`}`);
|
|
108
109
|
}
|
|
109
110
|
return;
|
|
110
111
|
}
|
package/dist/commands/env.js
CHANGED
|
@@ -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
|
-
|
|
25
|
+
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
const useV1 = !!config.apiKey;
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
+
process.exit(1);
|
|
46
53
|
}
|
|
47
54
|
const encryptionKey = getEncryptionKey();
|
|
48
55
|
const userId = getUserId();
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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}`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error-body access for main-API responses.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `src/lib/http/api-response.ts` `MainApiErrorBody` — keep in sync.
|
|
5
|
+
* The CLI cannot import from the app's `src/` (separate tsconfig / `rootDir`),
|
|
6
|
+
* so this is a deliberate type duplicate with the same shape and invariants.
|
|
7
|
+
* See `docs/api/error-handling.md` for the canonical envelope.
|
|
8
|
+
*
|
|
9
|
+
* Keep in sync with the other 2 copies:
|
|
10
|
+
* - src/lib/http/read-api-error-body.ts (main, canonical)
|
|
11
|
+
* - extension/src/lib/api-error-body.ts (browser extension)
|
|
12
|
+
* CI drift check: scripts/checks/check-api-error-body-drift.sh
|
|
13
|
+
*
|
|
14
|
+
* Note: OAuth endpoints (`/api/mcp/token`, `/api/mcp/register`) use the RFC 6749
|
|
15
|
+
* error envelope (`error`, `error_description`), NOT this shape. Reads from
|
|
16
|
+
* those endpoints belong in `cli/src/lib/oauth.ts` and are intentionally not
|
|
17
|
+
* migrated here.
|
|
18
|
+
*/
|
|
19
|
+
export type MainApiErrorBody = {
|
|
20
|
+
readonly error: string;
|
|
21
|
+
readonly details?: unknown;
|
|
22
|
+
readonly lockedUntil?: string | null;
|
|
23
|
+
readonly currentKeyVersion?: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function readApiErrorBody(res: Response): Promise<MainApiErrorBody | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Read `details.message` from an already-parsed error body.
|
|
28
|
+
*
|
|
29
|
+
* The `apiRequest` wrapper in `api-client.ts` returns `res.data` as the raw
|
|
30
|
+
* JSON body (success OR error shape). On error paths, callers can pass that
|
|
31
|
+
* body here after narrowing it via `readMainApiErrorBody`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getApiErrorMessage(body: MainApiErrorBody | null): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Read a single named field from `body.details` with a runtime type guard.
|
|
36
|
+
*
|
|
37
|
+
* Mirrors `getApiErrorDetail` in the main copy. See that file for full
|
|
38
|
+
* rationale.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getApiErrorDetail<T>(body: MainApiErrorBody | null, field: string, guard: (value: unknown) => value is T): T | null;
|
|
41
|
+
/**
|
|
42
|
+
* Read `details.properties[<field>].errors` from a Zod `treeifyError()` shape.
|
|
43
|
+
*
|
|
44
|
+
* Centralizes the per-field Zod-tree access pattern used by validation-error
|
|
45
|
+
* consumers (slug, url, etc.). Returns the errors array if present, `null`
|
|
46
|
+
* otherwise.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getApiErrorFieldErrors(body: MainApiErrorBody | null, field: string): readonly unknown[] | null;
|
|
49
|
+
/**
|
|
50
|
+
* Narrow an unknown value (e.g. `apiRequest`'s `res.data`) to `MainApiErrorBody`.
|
|
51
|
+
*
|
|
52
|
+
* Useful at error-path call sites that consume the wrapper's `res.data` rather
|
|
53
|
+
* than reading the `Response` directly.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readMainApiErrorBody(value: unknown): MainApiErrorBody | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error-body access for main-API responses.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `src/lib/http/api-response.ts` `MainApiErrorBody` — keep in sync.
|
|
5
|
+
* The CLI cannot import from the app's `src/` (separate tsconfig / `rootDir`),
|
|
6
|
+
* so this is a deliberate type duplicate with the same shape and invariants.
|
|
7
|
+
* See `docs/api/error-handling.md` for the canonical envelope.
|
|
8
|
+
*
|
|
9
|
+
* Keep in sync with the other 2 copies:
|
|
10
|
+
* - src/lib/http/read-api-error-body.ts (main, canonical)
|
|
11
|
+
* - extension/src/lib/api-error-body.ts (browser extension)
|
|
12
|
+
* CI drift check: scripts/checks/check-api-error-body-drift.sh
|
|
13
|
+
*
|
|
14
|
+
* Note: OAuth endpoints (`/api/mcp/token`, `/api/mcp/register`) use the RFC 6749
|
|
15
|
+
* error envelope (`error`, `error_description`), NOT this shape. Reads from
|
|
16
|
+
* those endpoints belong in `cli/src/lib/oauth.ts` and are intentionally not
|
|
17
|
+
* migrated here.
|
|
18
|
+
*/
|
|
19
|
+
export async function readApiErrorBody(res) {
|
|
20
|
+
if (res.ok)
|
|
21
|
+
return null;
|
|
22
|
+
const json = await res.json().catch(() => null);
|
|
23
|
+
if (!json ||
|
|
24
|
+
typeof json !== "object" ||
|
|
25
|
+
typeof json.error !== "string") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return json;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read `details.message` from an already-parsed error body.
|
|
32
|
+
*
|
|
33
|
+
* The `apiRequest` wrapper in `api-client.ts` returns `res.data` as the raw
|
|
34
|
+
* JSON body (success OR error shape). On error paths, callers can pass that
|
|
35
|
+
* body here after narrowing it via `readMainApiErrorBody`.
|
|
36
|
+
*/
|
|
37
|
+
export function getApiErrorMessage(body) {
|
|
38
|
+
if (!body || typeof body.details !== "object" || body.details === null) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const message = body.details.message;
|
|
42
|
+
return typeof message === "string" ? message : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read a single named field from `body.details` with a runtime type guard.
|
|
46
|
+
*
|
|
47
|
+
* Mirrors `getApiErrorDetail` in the main copy. See that file for full
|
|
48
|
+
* rationale.
|
|
49
|
+
*/
|
|
50
|
+
export function getApiErrorDetail(body, field, guard) {
|
|
51
|
+
if (!body || typeof body.details !== "object" || body.details === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const value = body.details[field];
|
|
55
|
+
return guard(value) ? value : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read `details.properties[<field>].errors` from a Zod `treeifyError()` shape.
|
|
59
|
+
*
|
|
60
|
+
* Centralizes the per-field Zod-tree access pattern used by validation-error
|
|
61
|
+
* consumers (slug, url, etc.). Returns the errors array if present, `null`
|
|
62
|
+
* otherwise.
|
|
63
|
+
*/
|
|
64
|
+
export function getApiErrorFieldErrors(body, field) {
|
|
65
|
+
if (!body || typeof body.details !== "object" || body.details === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const properties = body.details.properties;
|
|
69
|
+
if (!properties || typeof properties !== "object")
|
|
70
|
+
return null;
|
|
71
|
+
const fieldObj = properties[field];
|
|
72
|
+
if (!fieldObj || typeof fieldObj !== "object")
|
|
73
|
+
return null;
|
|
74
|
+
const errors = fieldObj.errors;
|
|
75
|
+
return Array.isArray(errors) ? errors : null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Narrow an unknown value (e.g. `apiRequest`'s `res.data`) to `MainApiErrorBody`.
|
|
79
|
+
*
|
|
80
|
+
* Useful at error-path call sites that consume the wrapper's `res.data` rather
|
|
81
|
+
* than reading the `Response` directly.
|
|
82
|
+
*/
|
|
83
|
+
export function readMainApiErrorBody(value) {
|
|
84
|
+
if (!value || typeof value !== "object")
|
|
85
|
+
return null;
|
|
86
|
+
if (typeof value.error !== "string")
|
|
87
|
+
return null;
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=api-error-body.js.map
|
|
@@ -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.
|