@mnemom/mnemom 0.10.0 → 0.11.0
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.d.ts +37 -0
- package/dist/commands/api-key.js +181 -0
- package/dist/commands/governance.d.ts +85 -0
- package/dist/commands/governance.js +331 -0
- package/dist/index.js +271 -0
- package/dist/lib/api.d.ts +183 -0
- package/dist/lib/api.js +208 -0
- package/dist/lib/format.d.ts +4 -0
- package/dist/lib/format.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom api-key ...` commands — capability-based API key management
|
|
3
|
+
* (ADR-049). Mirrors the dashboard scope picker and the API mint-time
|
|
4
|
+
* vocabulary; one CLI surface for the same key lifecycle operations.
|
|
5
|
+
*
|
|
6
|
+
* mnemom api-key list — list active personal keys
|
|
7
|
+
* mnemom api-key create --name <name> [--scopes a,b,c]
|
|
8
|
+
* — mint a new key
|
|
9
|
+
* mnemom api-key rotate <key_id> — atomic mint-new + revoke-old
|
|
10
|
+
* mnemom api-key revoke <key_id> — soft-delete
|
|
11
|
+
*
|
|
12
|
+
* Auth: any of the standard CLI auth methods — JWT (`mnemom login`),
|
|
13
|
+
* MNEMOM_API_KEY env, or a stored API key. Note the chicken-and-egg:
|
|
14
|
+
* to mint your first key you'll need a JWT (the website's create-key
|
|
15
|
+
* dialog is the alternative). After that, an `api:write` key can mint
|
|
16
|
+
* additional keys for itself.
|
|
17
|
+
*/
|
|
18
|
+
import { type ApiKeyScope } from "../lib/api.js";
|
|
19
|
+
/**
|
|
20
|
+
* Exported for tests. Parses a comma-separated `--scopes` flag value,
|
|
21
|
+
* accepts legacy `api` for backward compat, and returns a typed array.
|
|
22
|
+
* Throws on unknown scope names so users get a clear error instead of
|
|
23
|
+
* silently dropping intent.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseScopes(input: string | undefined): ApiKeyScope[];
|
|
26
|
+
export declare function apiKeyListCommand(opts: {
|
|
27
|
+
json?: boolean;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
export declare function apiKeyCreateCommand(opts: {
|
|
30
|
+
name?: string;
|
|
31
|
+
scopes?: string;
|
|
32
|
+
json?: boolean;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
export declare function apiKeyRotateCommand(keyId: string | undefined, opts: {
|
|
35
|
+
json?: boolean;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
export declare function apiKeyRevokeCommand(keyId: string | undefined): Promise<void>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom api-key ...` commands — capability-based API key management
|
|
3
|
+
* (ADR-049). Mirrors the dashboard scope picker and the API mint-time
|
|
4
|
+
* vocabulary; one CLI surface for the same key lifecycle operations.
|
|
5
|
+
*
|
|
6
|
+
* mnemom api-key list — list active personal keys
|
|
7
|
+
* mnemom api-key create --name <name> [--scopes a,b,c]
|
|
8
|
+
* — mint a new key
|
|
9
|
+
* mnemom api-key rotate <key_id> — atomic mint-new + revoke-old
|
|
10
|
+
* mnemom api-key revoke <key_id> — soft-delete
|
|
11
|
+
*
|
|
12
|
+
* Auth: any of the standard CLI auth methods — JWT (`mnemom login`),
|
|
13
|
+
* MNEMOM_API_KEY env, or a stored API key. Note the chicken-and-egg:
|
|
14
|
+
* to mint your first key you'll need a JWT (the website's create-key
|
|
15
|
+
* dialog is the alternative). After that, an `api:write` key can mint
|
|
16
|
+
* additional keys for itself.
|
|
17
|
+
*/
|
|
18
|
+
import { listApiKeys, createApiKey, rotateApiKey, revokeApiKey, isLegacyScopeSet, API_KEY_SCOPES, DEFAULT_API_KEY_SCOPES, } from "../lib/api.js";
|
|
19
|
+
import { requireAuth } from "../lib/auth.js";
|
|
20
|
+
import { fmt } from "../lib/format.js";
|
|
21
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Exported for tests. Parses a comma-separated `--scopes` flag value,
|
|
24
|
+
* accepts legacy `api` for backward compat, and returns a typed array.
|
|
25
|
+
* Throws on unknown scope names so users get a clear error instead of
|
|
26
|
+
* silently dropping intent.
|
|
27
|
+
*/
|
|
28
|
+
export function parseScopes(input) {
|
|
29
|
+
if (!input)
|
|
30
|
+
return [...DEFAULT_API_KEY_SCOPES];
|
|
31
|
+
const raw = input
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter((s) => s.length > 0);
|
|
35
|
+
if (raw.length === 0)
|
|
36
|
+
return [...DEFAULT_API_KEY_SCOPES];
|
|
37
|
+
const valid = new Set(API_KEY_SCOPES);
|
|
38
|
+
// Accept legacy 'api' input here so users who copy-paste from older
|
|
39
|
+
// docs/scripts get a clean canonicalization. The API also aliases.
|
|
40
|
+
valid.add("api");
|
|
41
|
+
const unknown = raw.filter((s) => !valid.has(s));
|
|
42
|
+
if (unknown.length > 0) {
|
|
43
|
+
throw new Error(`Unknown scope(s): ${unknown.join(", ")}. ` +
|
|
44
|
+
`Valid: ${Array.from(API_KEY_SCOPES).join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
return raw;
|
|
47
|
+
}
|
|
48
|
+
function renderScopeBadge(scope) {
|
|
49
|
+
// Admin scopes get a visual marker so the audit trail in the CLI
|
|
50
|
+
// makes elevated keys obvious at a glance.
|
|
51
|
+
if (scope === "admin:platform" || scope === "admin:org") {
|
|
52
|
+
return fmt.warn(`[${scope}]`);
|
|
53
|
+
}
|
|
54
|
+
return `[${scope}]`;
|
|
55
|
+
}
|
|
56
|
+
function formatRow(key) {
|
|
57
|
+
const created = key.created_at
|
|
58
|
+
? new Date(key.created_at).toISOString().slice(0, 10)
|
|
59
|
+
: "?";
|
|
60
|
+
const lastUsed = key.last_used_at
|
|
61
|
+
? new Date(key.last_used_at).toISOString().slice(0, 10)
|
|
62
|
+
: "never";
|
|
63
|
+
const scopes = (key.scopes ?? []).map(renderScopeBadge).join(" ");
|
|
64
|
+
const legacy = isLegacyScopeSet(key.scopes) ? fmt.dim(" (legacy)") : "";
|
|
65
|
+
return ` ${key.key_id} ${key.key_prefix}… ${key.name.padEnd(24)} ${scopes}${legacy}\n created ${created} · last used ${lastUsed}`;
|
|
66
|
+
}
|
|
67
|
+
// ─── mnemom api-key list ─────────────────────────────────────────────────
|
|
68
|
+
export async function apiKeyListCommand(opts) {
|
|
69
|
+
await requireAuth();
|
|
70
|
+
let keys;
|
|
71
|
+
try {
|
|
72
|
+
keys = await listApiKeys();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(fmt.error(`Failed to list api keys: ${err instanceof Error ? err.message : err}`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (opts.json) {
|
|
79
|
+
console.log(JSON.stringify(keys, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (keys.length === 0) {
|
|
83
|
+
console.log("\nNo active api keys.\n");
|
|
84
|
+
console.log("Create one with `mnemom api-key create --name <name>`.\n");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(fmt.header(`Active api keys (${keys.length})`));
|
|
89
|
+
console.log();
|
|
90
|
+
for (const k of keys) {
|
|
91
|
+
console.log(formatRow(k));
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ─── mnemom api-key create ───────────────────────────────────────────────
|
|
96
|
+
export async function apiKeyCreateCommand(opts) {
|
|
97
|
+
await requireAuth();
|
|
98
|
+
if (!opts.name || opts.name.trim() === "") {
|
|
99
|
+
console.error(fmt.error("--name is required. Example: --name 'ci-prod'"));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
let scopes;
|
|
103
|
+
try {
|
|
104
|
+
scopes = parseScopes(opts.scopes);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(fmt.error(err instanceof Error ? err.message : String(err)));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
let created;
|
|
111
|
+
try {
|
|
112
|
+
created = await createApiKey(opts.name.trim(), scopes);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
console.error(fmt.error(`Create failed: ${err instanceof Error ? err.message : err}`));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
console.log(JSON.stringify(created, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(fmt.success(`Created ${created.key_id}`));
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(fmt.label(" Name: ", ` ${created.name}`));
|
|
126
|
+
console.log(fmt.label(" Scopes: ", ` ${(created.scopes ?? []).join(", ")}`));
|
|
127
|
+
console.log(fmt.label(" Created: ", ` ${created.created_at}`));
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(fmt.warn(" Cleartext (shown once — copy now):"));
|
|
130
|
+
console.log(` ${created.key}`);
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(fmt.dim(" This is the only time the cleartext is returned."));
|
|
133
|
+
console.log(fmt.dim(" Mnemom stores only the SHA-256 hash; the key cannot be retrieved later."));
|
|
134
|
+
console.log();
|
|
135
|
+
}
|
|
136
|
+
// ─── mnemom api-key rotate ───────────────────────────────────────────────
|
|
137
|
+
export async function apiKeyRotateCommand(keyId, opts) {
|
|
138
|
+
await requireAuth();
|
|
139
|
+
if (!keyId) {
|
|
140
|
+
console.error(fmt.error("Usage: mnemom api-key rotate <key_id>"));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
let rotated;
|
|
144
|
+
try {
|
|
145
|
+
rotated = await rotateApiKey(keyId);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(fmt.error(`Rotate failed: ${err instanceof Error ? err.message : err}`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
if (opts.json) {
|
|
152
|
+
console.log(JSON.stringify(rotated, null, 2));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log();
|
|
156
|
+
console.log(fmt.success(`Rotated. Old key ${keyId} is now revoked.`));
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(fmt.label(" New key id:", ` ${rotated.key_id}`));
|
|
159
|
+
console.log(fmt.label(" Name: ", ` ${rotated.name}`));
|
|
160
|
+
console.log(fmt.label(" Scopes: ", ` ${(rotated.scopes ?? []).join(", ")}`));
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(fmt.warn(" New cleartext (shown once — copy now):"));
|
|
163
|
+
console.log(` ${rotated.key}`);
|
|
164
|
+
console.log();
|
|
165
|
+
}
|
|
166
|
+
// ─── mnemom api-key revoke ───────────────────────────────────────────────
|
|
167
|
+
export async function apiKeyRevokeCommand(keyId) {
|
|
168
|
+
await requireAuth();
|
|
169
|
+
if (!keyId) {
|
|
170
|
+
console.error(fmt.error("Usage: mnemom api-key revoke <key_id>"));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
await revokeApiKey(keyId);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.error(fmt.error(`Revoke failed: ${err instanceof Error ? err.message : err}`));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
console.log(fmt.success(`Revoked ${keyId}.`));
|
|
181
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom governance` — operator CLI for governance signals (ADR-048).
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
*
|
|
6
|
+
* signals list | show | ack | resolve | dismiss
|
|
7
|
+
* destinations list | add | remove | test
|
|
8
|
+
* rules list | add | remove
|
|
9
|
+
*
|
|
10
|
+
* The signal surface is operator-actionable: cron-driven sideband
|
|
11
|
+
* detections, future protection / posture observations. Distinct from
|
|
12
|
+
* `mnemom advisories list` (now narrowed to runtime.* / manual.* per
|
|
13
|
+
* ADR-048 §1).
|
|
14
|
+
*/
|
|
15
|
+
interface ListOpts {
|
|
16
|
+
org?: string;
|
|
17
|
+
team?: string;
|
|
18
|
+
agent?: string;
|
|
19
|
+
source?: string;
|
|
20
|
+
severity?: string;
|
|
21
|
+
status?: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
patternType?: string;
|
|
24
|
+
limit?: string;
|
|
25
|
+
since?: string;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface AckOpts {
|
|
29
|
+
action?: string;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface ResolveOpts {
|
|
33
|
+
status: string;
|
|
34
|
+
action?: string;
|
|
35
|
+
json?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface DismissOpts {
|
|
38
|
+
reason?: string;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface DestAddOpts {
|
|
42
|
+
org: string;
|
|
43
|
+
channel: string;
|
|
44
|
+
config: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
filter?: string;
|
|
47
|
+
json?: boolean;
|
|
48
|
+
}
|
|
49
|
+
interface RuleAddOpts {
|
|
50
|
+
org: string;
|
|
51
|
+
name: string;
|
|
52
|
+
predicate: string;
|
|
53
|
+
destinations: string;
|
|
54
|
+
json?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare function governanceSignalsListCommand(opts: ListOpts): Promise<void>;
|
|
57
|
+
export declare function governanceSignalsShowCommand(signalId: string, opts: {
|
|
58
|
+
json?: boolean;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
export declare function governanceSignalsAckCommand(signalId: string, opts: AckOpts): Promise<void>;
|
|
61
|
+
export declare function governanceSignalsResolveCommand(signalId: string, opts: ResolveOpts): Promise<void>;
|
|
62
|
+
export declare function governanceSignalsDismissCommand(signalId: string, opts: DismissOpts): Promise<void>;
|
|
63
|
+
export declare function governanceDestinationsListCommand(opts: {
|
|
64
|
+
org: string;
|
|
65
|
+
json?: boolean;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
export declare function governanceDestinationsAddCommand(opts: DestAddOpts): Promise<void>;
|
|
68
|
+
export declare function governanceDestinationsRemoveCommand(destinationId: string, opts: {
|
|
69
|
+
org: string;
|
|
70
|
+
json?: boolean;
|
|
71
|
+
}): Promise<void>;
|
|
72
|
+
export declare function governanceDestinationsTestCommand(destinationId: string, opts: {
|
|
73
|
+
org: string;
|
|
74
|
+
json?: boolean;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
export declare function governanceRulesListCommand(opts: {
|
|
77
|
+
org: string;
|
|
78
|
+
json?: boolean;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
export declare function governanceRulesAddCommand(opts: RuleAddOpts): Promise<void>;
|
|
81
|
+
export declare function governanceRulesRemoveCommand(ruleId: string, opts: {
|
|
82
|
+
org: string;
|
|
83
|
+
json?: boolean;
|
|
84
|
+
}): Promise<void>;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom governance` — operator CLI for governance signals (ADR-048).
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
*
|
|
6
|
+
* signals list | show | ack | resolve | dismiss
|
|
7
|
+
* destinations list | add | remove | test
|
|
8
|
+
* rules list | add | remove
|
|
9
|
+
*
|
|
10
|
+
* The signal surface is operator-actionable: cron-driven sideband
|
|
11
|
+
* detections, future protection / posture observations. Distinct from
|
|
12
|
+
* `mnemom advisories list` (now narrowed to runtime.* / manual.* per
|
|
13
|
+
* ADR-048 §1).
|
|
14
|
+
*/
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import { acknowledgeGovernanceSignal, createGovernanceDestination, createGovernanceRule, deleteGovernanceDestination, deleteGovernanceRule, dismissGovernanceSignal, getGovernanceSignal, listGovernanceDestinations, listGovernanceRules, listGovernanceSignalsForAgent, listGovernanceSignalsForOrg, listGovernanceSignalsForTeam, resolveGovernanceSignal, testGovernanceDestination, } from "../lib/api.js";
|
|
17
|
+
import { requireAuth } from "../lib/auth.js";
|
|
18
|
+
// ─── Formatting helpers ──────────────────────────────────────────────────
|
|
19
|
+
function shortId(id) {
|
|
20
|
+
return id.length <= 16 ? id : id.slice(0, 13) + "...";
|
|
21
|
+
}
|
|
22
|
+
function severityColor(s) {
|
|
23
|
+
switch (s) {
|
|
24
|
+
case "critical":
|
|
25
|
+
return chalk.bgRed.white;
|
|
26
|
+
case "high":
|
|
27
|
+
return chalk.red;
|
|
28
|
+
case "warn":
|
|
29
|
+
return chalk.yellow;
|
|
30
|
+
default:
|
|
31
|
+
return chalk.cyan;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function statusBadge(s) {
|
|
35
|
+
switch (s) {
|
|
36
|
+
case "open":
|
|
37
|
+
return chalk.bold.red("OPEN");
|
|
38
|
+
case "acknowledged":
|
|
39
|
+
return chalk.yellow("ACK");
|
|
40
|
+
case "resolved":
|
|
41
|
+
return chalk.green("RESOLVED");
|
|
42
|
+
case "dismissed":
|
|
43
|
+
return chalk.dim("DISMISSED");
|
|
44
|
+
case "expired":
|
|
45
|
+
return chalk.dim("expired");
|
|
46
|
+
default:
|
|
47
|
+
return chalk.dim(s.toUpperCase());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function formatSignalRow(s) {
|
|
51
|
+
const sev = severityColor(s.severity)(s.severity.toUpperCase().padEnd(8));
|
|
52
|
+
return [
|
|
53
|
+
chalk.dim(shortId(s.id).padEnd(16)),
|
|
54
|
+
sev,
|
|
55
|
+
chalk.cyan(s.source.padEnd(22)),
|
|
56
|
+
chalk.magenta((s.pattern_type || "—").slice(0, 28).padEnd(28)),
|
|
57
|
+
statusBadge(s.status).padEnd(20),
|
|
58
|
+
chalk.dim(s.detected_at.replace("T", " ").replace(/\.\d+Z$/, "Z")),
|
|
59
|
+
].join(" ");
|
|
60
|
+
}
|
|
61
|
+
// ─── signals list ────────────────────────────────────────────────────────
|
|
62
|
+
export async function governanceSignalsListCommand(opts) {
|
|
63
|
+
await requireAuth();
|
|
64
|
+
const filters = {
|
|
65
|
+
source: opts.source,
|
|
66
|
+
severity: opts.severity,
|
|
67
|
+
status: opts.status,
|
|
68
|
+
scope: opts.scope,
|
|
69
|
+
pattern_type: opts.patternType,
|
|
70
|
+
since: opts.since,
|
|
71
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : 100,
|
|
72
|
+
};
|
|
73
|
+
let result;
|
|
74
|
+
if (opts.org) {
|
|
75
|
+
result = await listGovernanceSignalsForOrg(opts.org, filters);
|
|
76
|
+
}
|
|
77
|
+
else if (opts.team) {
|
|
78
|
+
result = await listGovernanceSignalsForTeam(opts.team, filters);
|
|
79
|
+
}
|
|
80
|
+
else if (opts.agent) {
|
|
81
|
+
result = await listGovernanceSignalsForAgent(opts.agent, filters);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error("One of --org, --team, --agent is required.");
|
|
85
|
+
}
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
console.log(JSON.stringify(result, null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const signals = result.signals;
|
|
91
|
+
if (signals.length === 0) {
|
|
92
|
+
console.log(chalk.dim("(no governance signals match)"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log([
|
|
96
|
+
chalk.bold("ID".padEnd(16)),
|
|
97
|
+
chalk.bold("SEVERITY".padEnd(8)),
|
|
98
|
+
chalk.bold("SOURCE".padEnd(22)),
|
|
99
|
+
chalk.bold("PATTERN".padEnd(28)),
|
|
100
|
+
chalk.bold("STATUS".padEnd(20)),
|
|
101
|
+
chalk.bold("DETECTED"),
|
|
102
|
+
].join(" "));
|
|
103
|
+
for (const s of signals)
|
|
104
|
+
console.log(formatSignalRow(s));
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log(chalk.dim(`${signals.length} signal(s).`));
|
|
107
|
+
}
|
|
108
|
+
// ─── signals show ────────────────────────────────────────────────────────
|
|
109
|
+
export async function governanceSignalsShowCommand(signalId, opts) {
|
|
110
|
+
await requireAuth();
|
|
111
|
+
const signal = await getGovernanceSignal(signalId);
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
console.log(JSON.stringify(signal, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(chalk.bold(`Signal ${signal.id}`));
|
|
117
|
+
console.log(` scope: ${signal.scope} (${signal.scope_id})`);
|
|
118
|
+
console.log(` source: ${chalk.cyan(signal.source)}`);
|
|
119
|
+
console.log(` pattern: ${chalk.magenta(signal.pattern_type)}`);
|
|
120
|
+
console.log(` severity: ${severityColor(signal.severity)(signal.severity)}`);
|
|
121
|
+
console.log(` status: ${statusBadge(signal.status)}`);
|
|
122
|
+
console.log(` detected: ${signal.detected_at} by ${signal.detected_by}`);
|
|
123
|
+
console.log(` org: ${signal.org_id}`);
|
|
124
|
+
if (signal.team_id)
|
|
125
|
+
console.log(` team: ${signal.team_id}`);
|
|
126
|
+
console.log(` agents: ${signal.agent_ids.length === 0 ? "(none)" : signal.agent_ids.join(", ")}`);
|
|
127
|
+
if (signal.acknowledged_at) {
|
|
128
|
+
console.log(` ack: ${signal.acknowledged_at} (${signal.acknowledged_actor_role ?? "?"})${signal.acknowledged_by ? ` by ${signal.acknowledged_by}` : ""}`);
|
|
129
|
+
}
|
|
130
|
+
if (signal.resolved_at) {
|
|
131
|
+
console.log(` resolved: ${signal.resolved_at} (${signal.resolution_status ?? "?"})${signal.action_taken ? ` — ${signal.action_taken}` : ""}`);
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(signal.detail ?? {}).length > 0) {
|
|
134
|
+
console.log(chalk.dim(" detail:"));
|
|
135
|
+
console.log(JSON.stringify(signal.detail, null, 2)
|
|
136
|
+
.split("\n")
|
|
137
|
+
.map((l) => " " + l)
|
|
138
|
+
.join("\n"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ─── signals ack/resolve/dismiss ─────────────────────────────────────────
|
|
142
|
+
export async function governanceSignalsAckCommand(signalId, opts) {
|
|
143
|
+
await requireAuth();
|
|
144
|
+
const signal = await acknowledgeGovernanceSignal(signalId, {
|
|
145
|
+
action_taken: opts.action,
|
|
146
|
+
});
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(JSON.stringify(signal, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
console.log(chalk.green(`✓ Acknowledged ${signal.id} as ${signal.acknowledged_actor_role}.`));
|
|
152
|
+
}
|
|
153
|
+
export async function governanceSignalsResolveCommand(signalId, opts) {
|
|
154
|
+
await requireAuth();
|
|
155
|
+
const valid = [
|
|
156
|
+
"action_taken",
|
|
157
|
+
"wont_fix",
|
|
158
|
+
"duplicate",
|
|
159
|
+
"false_positive",
|
|
160
|
+
"self_resolved",
|
|
161
|
+
];
|
|
162
|
+
if (!valid.includes(opts.status)) {
|
|
163
|
+
throw new Error(`--status must be one of: ${valid.join(", ")}`);
|
|
164
|
+
}
|
|
165
|
+
const signal = await resolveGovernanceSignal(signalId, {
|
|
166
|
+
resolution_status: opts.status,
|
|
167
|
+
action_taken: opts.action,
|
|
168
|
+
});
|
|
169
|
+
if (opts.json) {
|
|
170
|
+
console.log(JSON.stringify(signal, null, 2));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log(chalk.green(`✓ Resolved ${signal.id} (${signal.resolution_status}).`));
|
|
174
|
+
}
|
|
175
|
+
export async function governanceSignalsDismissCommand(signalId, opts) {
|
|
176
|
+
await requireAuth();
|
|
177
|
+
const signal = await dismissGovernanceSignal(signalId, { reason: opts.reason });
|
|
178
|
+
if (opts.json) {
|
|
179
|
+
console.log(JSON.stringify(signal, null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.log(chalk.dim(`✓ Dismissed ${signal.id}.`));
|
|
183
|
+
}
|
|
184
|
+
// ─── destinations ────────────────────────────────────────────────────────
|
|
185
|
+
function formatDestinationRow(d) {
|
|
186
|
+
return [
|
|
187
|
+
chalk.dim(shortId(d.id).padEnd(16)),
|
|
188
|
+
chalk.cyan(d.channel.padEnd(10)),
|
|
189
|
+
(d.display_name ?? "(unnamed)").slice(0, 32).padEnd(32),
|
|
190
|
+
d.enabled ? chalk.green("enabled") : chalk.dim("disabled"),
|
|
191
|
+
d.last_test_status === "ok"
|
|
192
|
+
? chalk.green(`tested ✓ ${d.last_tested_at?.slice(0, 19) ?? ""}`)
|
|
193
|
+
: d.last_test_status === "failed"
|
|
194
|
+
? chalk.red(`tested ✗ ${d.last_test_error?.slice(0, 64) ?? ""}`)
|
|
195
|
+
: chalk.dim("untested"),
|
|
196
|
+
].join(" ");
|
|
197
|
+
}
|
|
198
|
+
export async function governanceDestinationsListCommand(opts) {
|
|
199
|
+
await requireAuth();
|
|
200
|
+
const result = await listGovernanceDestinations(opts.org);
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify(result, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result.destinations.length === 0) {
|
|
206
|
+
console.log(chalk.dim("(no destinations configured for this org)"));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const d of result.destinations)
|
|
210
|
+
console.log(formatDestinationRow(d));
|
|
211
|
+
}
|
|
212
|
+
export async function governanceDestinationsAddCommand(opts) {
|
|
213
|
+
await requireAuth();
|
|
214
|
+
const validChannels = [
|
|
215
|
+
"webhook",
|
|
216
|
+
"slack",
|
|
217
|
+
"email",
|
|
218
|
+
"pagerduty",
|
|
219
|
+
];
|
|
220
|
+
if (!validChannels.includes(opts.channel)) {
|
|
221
|
+
throw new Error(`--channel must be one of: ${validChannels.join(", ")}`);
|
|
222
|
+
}
|
|
223
|
+
let config;
|
|
224
|
+
try {
|
|
225
|
+
config = JSON.parse(opts.config);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
throw new Error("--config must be valid JSON");
|
|
229
|
+
}
|
|
230
|
+
let filter;
|
|
231
|
+
if (opts.filter) {
|
|
232
|
+
try {
|
|
233
|
+
filter = JSON.parse(opts.filter);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw new Error("--filter must be valid JSON");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const dest = await createGovernanceDestination(opts.org, {
|
|
240
|
+
channel: opts.channel,
|
|
241
|
+
config,
|
|
242
|
+
filter,
|
|
243
|
+
display_name: opts.name,
|
|
244
|
+
});
|
|
245
|
+
if (opts.json) {
|
|
246
|
+
console.log(JSON.stringify(dest, null, 2));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.log(chalk.green(`✓ Created destination ${dest.id} (${dest.channel}).`));
|
|
250
|
+
}
|
|
251
|
+
export async function governanceDestinationsRemoveCommand(destinationId, opts) {
|
|
252
|
+
await requireAuth();
|
|
253
|
+
await deleteGovernanceDestination(opts.org, destinationId);
|
|
254
|
+
if (opts.json) {
|
|
255
|
+
console.log(JSON.stringify({ ok: true, removed: destinationId }, null, 2));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(chalk.green(`✓ Removed destination ${destinationId}.`));
|
|
259
|
+
}
|
|
260
|
+
export async function governanceDestinationsTestCommand(destinationId, opts) {
|
|
261
|
+
await requireAuth();
|
|
262
|
+
const result = await testGovernanceDestination(opts.org, destinationId);
|
|
263
|
+
if (opts.json) {
|
|
264
|
+
console.log(JSON.stringify(result, null, 2));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (result.result.ok) {
|
|
268
|
+
console.log(chalk.green(`✓ Test signal delivered via ${result.channel} (${result.result.attempts} attempt).`));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.log(chalk.red(`✗ Test failed via ${result.channel}: ${result.result.last_error ?? "unknown"}`));
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ─── rules ───────────────────────────────────────────────────────────────
|
|
276
|
+
function formatRuleRow(r) {
|
|
277
|
+
return [
|
|
278
|
+
chalk.dim(shortId(r.id).padEnd(16)),
|
|
279
|
+
r.name.slice(0, 32).padEnd(32),
|
|
280
|
+
r.enabled ? chalk.green("enabled") : chalk.dim("disabled"),
|
|
281
|
+
chalk.dim(`fired ${r.fire_count}×`),
|
|
282
|
+
r.last_fired_at ? chalk.dim(`last ${r.last_fired_at.slice(0, 19)}`) : "",
|
|
283
|
+
].join(" ");
|
|
284
|
+
}
|
|
285
|
+
export async function governanceRulesListCommand(opts) {
|
|
286
|
+
await requireAuth();
|
|
287
|
+
const result = await listGovernanceRules(opts.org);
|
|
288
|
+
if (opts.json) {
|
|
289
|
+
console.log(JSON.stringify(result, null, 2));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (result.rules.length === 0) {
|
|
293
|
+
console.log(chalk.dim("(no escalation rules configured for this org)"));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for (const r of result.rules)
|
|
297
|
+
console.log(formatRuleRow(r));
|
|
298
|
+
}
|
|
299
|
+
export async function governanceRulesAddCommand(opts) {
|
|
300
|
+
await requireAuth();
|
|
301
|
+
let predicate;
|
|
302
|
+
try {
|
|
303
|
+
predicate = JSON.parse(opts.predicate);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
throw new Error("--predicate must be valid JSON");
|
|
307
|
+
}
|
|
308
|
+
const destinationIds = opts.destinations.split(",").map((s) => s.trim()).filter(Boolean);
|
|
309
|
+
if (destinationIds.length === 0) {
|
|
310
|
+
throw new Error("--destinations must list at least one destination ID (comma-separated)");
|
|
311
|
+
}
|
|
312
|
+
const rule = await createGovernanceRule(opts.org, {
|
|
313
|
+
name: opts.name,
|
|
314
|
+
predicate,
|
|
315
|
+
destination_ids: destinationIds,
|
|
316
|
+
});
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
console.log(JSON.stringify(rule, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
console.log(chalk.green(`✓ Created rule ${rule.id} ("${rule.name}").`));
|
|
322
|
+
}
|
|
323
|
+
export async function governanceRulesRemoveCommand(ruleId, opts) {
|
|
324
|
+
await requireAuth();
|
|
325
|
+
await deleteGovernanceRule(opts.org, ruleId);
|
|
326
|
+
if (opts.json) {
|
|
327
|
+
console.log(JSON.stringify({ ok: true, removed: ruleId }, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.log(chalk.green(`✓ Removed rule ${ruleId}.`));
|
|
331
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { advisoriesListCommand, advisoriesShowCommand, } from "./commands/adviso
|
|
|
14
14
|
import { postureListCommand, postureShowCommand, postureCreateCommand, postureUpdateCommand, postureCloneCommand, postureRevisionsCommand, postureDiffCommand, postureAssignCommand, postureUnassignCommand, posturePreviewComposeCommand, postureDeleteCommand, } from "./commands/posture.js";
|
|
15
15
|
import { loginCommand, logoutCommand, whoamiCommand } from "./commands/auth.js";
|
|
16
16
|
import { validateSafeHouseCommand } from "./commands/validate.js";
|
|
17
|
+
import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRotateCommand, apiKeyRevokeCommand, } from "./commands/api-key.js";
|
|
17
18
|
program
|
|
18
19
|
.name("mnemom")
|
|
19
20
|
.description("Transparent AI agent tracing")
|
|
@@ -521,6 +522,212 @@ advisoriesCmd
|
|
|
521
522
|
}
|
|
522
523
|
});
|
|
523
524
|
// ============================================================================
|
|
525
|
+
// Governance signals (ADR-048) — operator-actionable observation surface
|
|
526
|
+
//
|
|
527
|
+
// Distinct from `mnemom advisories list` (which now narrows to runtime.* /
|
|
528
|
+
// manual.* per ADR-048 §1). Surfaces sideband.* + future protection.* /
|
|
529
|
+
// posture.* signals from governance_signals: ack/resolve/dismiss workflow,
|
|
530
|
+
// notification destinations (slack/email/pagerduty/webhook), escalation
|
|
531
|
+
// rules.
|
|
532
|
+
// ============================================================================
|
|
533
|
+
import { governanceSignalsListCommand, governanceSignalsShowCommand, governanceSignalsAckCommand, governanceSignalsResolveCommand, governanceSignalsDismissCommand, governanceDestinationsListCommand, governanceDestinationsAddCommand, governanceDestinationsRemoveCommand, governanceDestinationsTestCommand, governanceRulesListCommand, governanceRulesAddCommand, governanceRulesRemoveCommand, } from "./commands/governance.js";
|
|
534
|
+
const governanceCmd = program
|
|
535
|
+
.command("governance")
|
|
536
|
+
.description("Operator workflow for governance_signals (ADR-048)");
|
|
537
|
+
const govSignalsCmd = governanceCmd
|
|
538
|
+
.command("signals")
|
|
539
|
+
.description("List, inspect, and transition governance_signals");
|
|
540
|
+
govSignalsCmd
|
|
541
|
+
.command("list")
|
|
542
|
+
.description("List governance signals at platform/org/team/agent scope")
|
|
543
|
+
.option("--org <id>", "Org scope")
|
|
544
|
+
.option("--team <id>", "Team scope")
|
|
545
|
+
.option("--agent <id>", "Per-agent slice (signals affecting this agent)")
|
|
546
|
+
.option("--source <s>", "Filter by source (sideband.{drift,coherence,fault_line,fleet})")
|
|
547
|
+
.option("--severity <s>", "Filter by severity (info|warn|high|critical)")
|
|
548
|
+
.option("--status <s>", "Filter by status (open|acknowledged|resolved|dismissed|expired)")
|
|
549
|
+
.option("--scope <s>", "Filter by scope (platform|org|team|agent)")
|
|
550
|
+
.option("--pattern-type <s>", "Filter by pattern_type")
|
|
551
|
+
.option("--since <iso>", "Detected since (ISO timestamp)")
|
|
552
|
+
.option("--limit <n>", "Max rows (default 100, max 500)")
|
|
553
|
+
.option("--json", "Output JSON")
|
|
554
|
+
.action(async (options) => {
|
|
555
|
+
try {
|
|
556
|
+
await governanceSignalsListCommand(options);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
govSignalsCmd
|
|
564
|
+
.command("show <signal_id>")
|
|
565
|
+
.description("Show one signal in detail")
|
|
566
|
+
.option("--json", "Output JSON")
|
|
567
|
+
.action(async (signalId, options) => {
|
|
568
|
+
try {
|
|
569
|
+
await governanceSignalsShowCommand(signalId, options);
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
govSignalsCmd
|
|
577
|
+
.command("ack <signal_id>")
|
|
578
|
+
.description("Acknowledge an open signal (org_admin / org_owner)")
|
|
579
|
+
.option("--action <text>", "Operator-authored action note")
|
|
580
|
+
.option("--json", "Output JSON")
|
|
581
|
+
.action(async (signalId, options) => {
|
|
582
|
+
try {
|
|
583
|
+
await governanceSignalsAckCommand(signalId, options);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
govSignalsCmd
|
|
591
|
+
.command("resolve <signal_id>")
|
|
592
|
+
.description("Resolve a signal with a resolution status")
|
|
593
|
+
.requiredOption("--status <s>", "Resolution status (action_taken|wont_fix|duplicate|false_positive|self_resolved)")
|
|
594
|
+
.option("--action <text>", "Operator-authored action note")
|
|
595
|
+
.option("--json", "Output JSON")
|
|
596
|
+
.action(async (signalId, options) => {
|
|
597
|
+
try {
|
|
598
|
+
await governanceSignalsResolveCommand(signalId, options);
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
govSignalsCmd
|
|
606
|
+
.command("dismiss <signal_id>")
|
|
607
|
+
.description("Dismiss a signal as not actionable")
|
|
608
|
+
.option("--reason <text>", "Operator-authored dismissal reason")
|
|
609
|
+
.option("--json", "Output JSON")
|
|
610
|
+
.action(async (signalId, options) => {
|
|
611
|
+
try {
|
|
612
|
+
await governanceSignalsDismissCommand(signalId, options);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
const govDestCmd = governanceCmd
|
|
620
|
+
.command("destinations")
|
|
621
|
+
.description("Manage notification destinations (slack/email/pagerduty/webhook)");
|
|
622
|
+
govDestCmd
|
|
623
|
+
.command("list")
|
|
624
|
+
.description("List destinations for an org")
|
|
625
|
+
.requiredOption("--org <id>", "Org ID")
|
|
626
|
+
.option("--json", "Output JSON")
|
|
627
|
+
.action(async (options) => {
|
|
628
|
+
try {
|
|
629
|
+
await governanceDestinationsListCommand(options);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
govDestCmd
|
|
637
|
+
.command("add")
|
|
638
|
+
.description("Add a destination (channel + JSON config)")
|
|
639
|
+
.requiredOption("--org <id>", "Org ID")
|
|
640
|
+
.requiredOption("--channel <c>", "webhook|slack|email|pagerduty")
|
|
641
|
+
.requiredOption("--config <json>", "Channel config (e.g., '{\"url\":\"...\",\"signing_secret\":\"...\"}')")
|
|
642
|
+
.option("--name <n>", "Display name")
|
|
643
|
+
.option("--filter <json>", "Filter narrowing (sources/severities/etc)")
|
|
644
|
+
.option("--json", "Output JSON")
|
|
645
|
+
.action(async (options) => {
|
|
646
|
+
try {
|
|
647
|
+
await governanceDestinationsAddCommand(options);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
govDestCmd
|
|
655
|
+
.command("remove <destination_id>")
|
|
656
|
+
.description("Remove a destination")
|
|
657
|
+
.requiredOption("--org <id>", "Org ID")
|
|
658
|
+
.option("--json", "Output JSON")
|
|
659
|
+
.action(async (id, options) => {
|
|
660
|
+
try {
|
|
661
|
+
await governanceDestinationsRemoveCommand(id, options);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
govDestCmd
|
|
669
|
+
.command("test <destination_id>")
|
|
670
|
+
.description("Send a synthetic test signal through this destination")
|
|
671
|
+
.requiredOption("--org <id>", "Org ID")
|
|
672
|
+
.option("--json", "Output JSON")
|
|
673
|
+
.action(async (id, options) => {
|
|
674
|
+
try {
|
|
675
|
+
await governanceDestinationsTestCommand(id, options);
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
const govRulesCmd = governanceCmd
|
|
683
|
+
.command("rules")
|
|
684
|
+
.description("Manage escalation rules (predicate → destinations)");
|
|
685
|
+
govRulesCmd
|
|
686
|
+
.command("list")
|
|
687
|
+
.description("List escalation rules for an org")
|
|
688
|
+
.requiredOption("--org <id>", "Org ID")
|
|
689
|
+
.option("--json", "Output JSON")
|
|
690
|
+
.action(async (options) => {
|
|
691
|
+
try {
|
|
692
|
+
await governanceRulesListCommand(options);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
govRulesCmd
|
|
700
|
+
.command("add")
|
|
701
|
+
.description("Add an escalation rule (predicate JSON + destination IDs)")
|
|
702
|
+
.requiredOption("--org <id>", "Org ID")
|
|
703
|
+
.requiredOption("--name <n>", "Rule name")
|
|
704
|
+
.requiredOption("--predicate <json>", "Predicate JSON (e.g., '{\"source\":\"sideband.fleet\",\"severity_min\":\"high\"}')")
|
|
705
|
+
.requiredOption("--destinations <ids>", "Comma-separated destination IDs")
|
|
706
|
+
.option("--json", "Output JSON")
|
|
707
|
+
.action(async (options) => {
|
|
708
|
+
try {
|
|
709
|
+
await governanceRulesAddCommand(options);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
govRulesCmd
|
|
717
|
+
.command("remove <rule_id>")
|
|
718
|
+
.description("Remove an escalation rule")
|
|
719
|
+
.requiredOption("--org <id>", "Org ID")
|
|
720
|
+
.option("--json", "Output JSON")
|
|
721
|
+
.action(async (id, options) => {
|
|
722
|
+
try {
|
|
723
|
+
await governanceRulesRemoveCommand(id, options);
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
// ============================================================================
|
|
524
731
|
// Trust Posture (Piece 3 of T1-3.1, ADR-045)
|
|
525
732
|
//
|
|
526
733
|
// Postures are team-scoped policy input that drives the observer's sideband
|
|
@@ -764,4 +971,68 @@ validateCmd
|
|
|
764
971
|
process.exit(1);
|
|
765
972
|
}
|
|
766
973
|
});
|
|
974
|
+
// ── api-key (ADR-049 capability-based scopes) ────────────────────────────
|
|
975
|
+
const apiKeyCmd = program
|
|
976
|
+
.command("api-key")
|
|
977
|
+
.description("Manage personal API keys (capability-based scopes — ADR-049)");
|
|
978
|
+
apiKeyCmd
|
|
979
|
+
.command("list")
|
|
980
|
+
.description("List your active personal API keys with their scope sets")
|
|
981
|
+
.option("--json", "Emit raw JSON instead of rendered output")
|
|
982
|
+
.action(async (options) => {
|
|
983
|
+
try {
|
|
984
|
+
await apiKeyListCommand({ json: options.json });
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
apiKeyCmd
|
|
992
|
+
.command("create")
|
|
993
|
+
.description("Mint a new personal API key. Default scopes: gateway, api:read, api:write. " +
|
|
994
|
+
"Admin scopes (admin:org, admin:platform) are role-gated at mint time.")
|
|
995
|
+
.requiredOption("--name <name>", "Friendly name for the key (e.g. 'ci-prod')")
|
|
996
|
+
.option("--scopes <list>", "Comma-separated capability scopes. Valid: " +
|
|
997
|
+
"gateway, api:read, api:write, admin:org, admin:platform. " +
|
|
998
|
+
"Default: gateway,api:read,api:write")
|
|
999
|
+
.option("--json", "Emit raw JSON instead of rendered output")
|
|
1000
|
+
.action(async (options) => {
|
|
1001
|
+
try {
|
|
1002
|
+
await apiKeyCreateCommand({
|
|
1003
|
+
name: options.name,
|
|
1004
|
+
scopes: options.scopes,
|
|
1005
|
+
json: options.json,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
apiKeyCmd
|
|
1014
|
+
.command("rotate <key_id>")
|
|
1015
|
+
.description("Atomic mint-new + revoke-old. Returns the new cleartext once.")
|
|
1016
|
+
.option("--json", "Emit raw JSON instead of rendered output")
|
|
1017
|
+
.action(async (keyId, options) => {
|
|
1018
|
+
try {
|
|
1019
|
+
await apiKeyRotateCommand(keyId, { json: options.json });
|
|
1020
|
+
}
|
|
1021
|
+
catch (error) {
|
|
1022
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
apiKeyCmd
|
|
1027
|
+
.command("revoke <key_id>")
|
|
1028
|
+
.description("Soft-revoke an active key. Action is immediate and irreversible.")
|
|
1029
|
+
.action(async (keyId) => {
|
|
1030
|
+
try {
|
|
1031
|
+
await apiKeyRevokeCommand(keyId);
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
767
1038
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -645,4 +645,187 @@ export declare function getSafeHouseHarnessState(): Promise<{
|
|
|
645
645
|
full: HarnessRunSummary | null;
|
|
646
646
|
fast: HarnessRunSummary | null;
|
|
647
647
|
}>;
|
|
648
|
+
export type GovernanceSignalScope = "platform" | "org" | "team" | "agent";
|
|
649
|
+
export type GovernanceSignalSource = "sideband.drift" | "sideband.coherence" | "sideband.fault_line" | "sideband.fleet";
|
|
650
|
+
export type GovernanceSignalSeverity = "info" | "warn" | "high" | "critical";
|
|
651
|
+
export type GovernanceSignalStatus = "open" | "acknowledged" | "resolved" | "dismissed" | "expired";
|
|
652
|
+
export type GovernanceResolutionStatus = "action_taken" | "wont_fix" | "duplicate" | "false_positive" | "self_resolved";
|
|
653
|
+
export type GovernanceNotificationChannel = "webhook" | "slack" | "email" | "pagerduty";
|
|
654
|
+
export interface GovernanceSignal {
|
|
655
|
+
id: string;
|
|
656
|
+
scope: GovernanceSignalScope;
|
|
657
|
+
scope_id: string;
|
|
658
|
+
source: GovernanceSignalSource;
|
|
659
|
+
pattern_type: string;
|
|
660
|
+
severity: GovernanceSignalSeverity;
|
|
661
|
+
detected_at: string;
|
|
662
|
+
detected_by: string;
|
|
663
|
+
org_id: string;
|
|
664
|
+
team_id: string | null;
|
|
665
|
+
agent_ids: string[];
|
|
666
|
+
detail: Record<string, unknown>;
|
|
667
|
+
source_ref: Record<string, unknown>;
|
|
668
|
+
status: GovernanceSignalStatus;
|
|
669
|
+
acknowledged_by: string | null;
|
|
670
|
+
acknowledged_at: string | null;
|
|
671
|
+
acknowledged_actor_role: string | null;
|
|
672
|
+
resolution_status: GovernanceResolutionStatus | null;
|
|
673
|
+
action_taken: string | null;
|
|
674
|
+
resolved_by: string | null;
|
|
675
|
+
resolved_at: string | null;
|
|
676
|
+
expires_at: string | null;
|
|
677
|
+
notification_state: Record<string, unknown>;
|
|
678
|
+
created_at: string;
|
|
679
|
+
updated_at: string;
|
|
680
|
+
}
|
|
681
|
+
export interface GovernanceDestination {
|
|
682
|
+
id: string;
|
|
683
|
+
org_id: string;
|
|
684
|
+
channel: GovernanceNotificationChannel;
|
|
685
|
+
config: Record<string, unknown>;
|
|
686
|
+
filter: Record<string, unknown>;
|
|
687
|
+
enabled: boolean;
|
|
688
|
+
display_name: string | null;
|
|
689
|
+
last_tested_at: string | null;
|
|
690
|
+
last_test_status: "ok" | "failed" | null;
|
|
691
|
+
last_test_error: string | null;
|
|
692
|
+
created_at: string;
|
|
693
|
+
updated_at: string;
|
|
694
|
+
}
|
|
695
|
+
export interface GovernanceRule {
|
|
696
|
+
id: string;
|
|
697
|
+
org_id: string;
|
|
698
|
+
name: string;
|
|
699
|
+
predicate: Record<string, unknown>;
|
|
700
|
+
destination_ids: string[];
|
|
701
|
+
enabled: boolean;
|
|
702
|
+
last_fired_at: string | null;
|
|
703
|
+
fire_count: number;
|
|
704
|
+
created_at: string;
|
|
705
|
+
updated_at: string;
|
|
706
|
+
}
|
|
707
|
+
interface SignalListFilters {
|
|
708
|
+
source?: string;
|
|
709
|
+
severity?: string;
|
|
710
|
+
status?: string;
|
|
711
|
+
scope?: string;
|
|
712
|
+
pattern_type?: string;
|
|
713
|
+
since?: string;
|
|
714
|
+
limit?: number;
|
|
715
|
+
}
|
|
716
|
+
export declare function listGovernanceSignalsForOrg(orgId: string, opts?: SignalListFilters): Promise<{
|
|
717
|
+
org_id: string;
|
|
718
|
+
signals: GovernanceSignal[];
|
|
719
|
+
}>;
|
|
720
|
+
export declare function listGovernanceSignalsForTeam(teamId: string, opts?: SignalListFilters): Promise<{
|
|
721
|
+
team_id: string;
|
|
722
|
+
signals: GovernanceSignal[];
|
|
723
|
+
}>;
|
|
724
|
+
export declare function listGovernanceSignalsForAgent(agentId: string, opts?: SignalListFilters): Promise<{
|
|
725
|
+
agent_id: string;
|
|
726
|
+
signals: GovernanceSignal[];
|
|
727
|
+
}>;
|
|
728
|
+
export declare function getGovernanceSignal(id: string): Promise<GovernanceSignal>;
|
|
729
|
+
export declare function acknowledgeGovernanceSignal(id: string, body?: {
|
|
730
|
+
action_taken?: string;
|
|
731
|
+
}): Promise<GovernanceSignal>;
|
|
732
|
+
export declare function resolveGovernanceSignal(id: string, body: {
|
|
733
|
+
resolution_status: GovernanceResolutionStatus;
|
|
734
|
+
action_taken?: string;
|
|
735
|
+
}): Promise<GovernanceSignal>;
|
|
736
|
+
export declare function dismissGovernanceSignal(id: string, body?: {
|
|
737
|
+
reason?: string;
|
|
738
|
+
}): Promise<GovernanceSignal>;
|
|
739
|
+
export declare function listGovernanceDestinations(orgId: string): Promise<{
|
|
740
|
+
org_id: string;
|
|
741
|
+
destinations: GovernanceDestination[];
|
|
742
|
+
}>;
|
|
743
|
+
export declare function createGovernanceDestination(orgId: string, body: {
|
|
744
|
+
channel: GovernanceNotificationChannel;
|
|
745
|
+
config: Record<string, unknown>;
|
|
746
|
+
filter?: Record<string, unknown>;
|
|
747
|
+
display_name?: string;
|
|
748
|
+
enabled?: boolean;
|
|
749
|
+
}): Promise<GovernanceDestination>;
|
|
750
|
+
export declare function deleteGovernanceDestination(orgId: string, destinationId: string): Promise<{
|
|
751
|
+
ok: true;
|
|
752
|
+
}>;
|
|
753
|
+
export declare function testGovernanceDestination(orgId: string, destinationId: string): Promise<{
|
|
754
|
+
destination_id: string;
|
|
755
|
+
channel: GovernanceNotificationChannel;
|
|
756
|
+
result: {
|
|
757
|
+
ok: boolean;
|
|
758
|
+
attempts: number;
|
|
759
|
+
delivered_at?: string;
|
|
760
|
+
last_error?: string;
|
|
761
|
+
};
|
|
762
|
+
}>;
|
|
763
|
+
export declare function listGovernanceRules(orgId: string): Promise<{
|
|
764
|
+
org_id: string;
|
|
765
|
+
rules: GovernanceRule[];
|
|
766
|
+
}>;
|
|
767
|
+
export declare function createGovernanceRule(orgId: string, body: {
|
|
768
|
+
name: string;
|
|
769
|
+
predicate: Record<string, unknown>;
|
|
770
|
+
destination_ids: string[];
|
|
771
|
+
enabled?: boolean;
|
|
772
|
+
}): Promise<GovernanceRule>;
|
|
773
|
+
export declare function deleteGovernanceRule(orgId: string, ruleId: string): Promise<{
|
|
774
|
+
ok: true;
|
|
775
|
+
}>;
|
|
776
|
+
/**
|
|
777
|
+
* Capability-based scope vocabulary (ADR-049). Mirrors the API's
|
|
778
|
+
* VALID_SCOPES exactly. Update both surfaces together when the
|
|
779
|
+
* vocabulary changes.
|
|
780
|
+
*/
|
|
781
|
+
export declare const API_KEY_SCOPES: readonly ["gateway", "api:read", "api:write", "admin:org", "admin:platform"];
|
|
782
|
+
export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
|
|
783
|
+
export declare const DEFAULT_API_KEY_SCOPES: ApiKeyScope[];
|
|
784
|
+
/**
|
|
785
|
+
* Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
|
|
786
|
+
* keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
|
|
787
|
+
* for display purposes; the auth gate handles the actual aliasing.
|
|
788
|
+
*/
|
|
789
|
+
export declare function isLegacyScopeSet(scopes: string[] | undefined): boolean;
|
|
790
|
+
export interface ApiKeyListItem {
|
|
791
|
+
key_id: string;
|
|
792
|
+
key_prefix: string;
|
|
793
|
+
name: string;
|
|
794
|
+
scopes: string[];
|
|
795
|
+
created_at: string;
|
|
796
|
+
last_used_at: string | null;
|
|
797
|
+
org_id?: string | null;
|
|
798
|
+
}
|
|
799
|
+
export interface ApiKeyCreated {
|
|
800
|
+
key_id: string;
|
|
801
|
+
key: string;
|
|
802
|
+
key_prefix: string;
|
|
803
|
+
name: string;
|
|
804
|
+
scopes: string[];
|
|
805
|
+
created_at: string;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* GET /v1/api-keys — list the caller's active personal API keys.
|
|
809
|
+
*/
|
|
810
|
+
export declare function listApiKeys(): Promise<ApiKeyListItem[]>;
|
|
811
|
+
/**
|
|
812
|
+
* POST /v1/api-keys — mint a new personal API key with explicit scopes.
|
|
813
|
+
*
|
|
814
|
+
* Returns the full secret only on this call. The mint-time ceiling
|
|
815
|
+
* rejects admin scopes the caller is not eligible for (admin:platform
|
|
816
|
+
* for non-staff, admin:org for users not in any org-admin role).
|
|
817
|
+
*/
|
|
818
|
+
export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promise<ApiKeyCreated>;
|
|
819
|
+
/**
|
|
820
|
+
* POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
|
|
821
|
+
* Returns the full new secret only on this call. The new key inherits
|
|
822
|
+
* the old key's name and scopes verbatim; the old key is revoked the
|
|
823
|
+
* moment this returns.
|
|
824
|
+
*/
|
|
825
|
+
export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
|
|
826
|
+
/**
|
|
827
|
+
* DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
|
|
828
|
+
* audit; `is_active` flips to false and `revoked_at` is timestamped.
|
|
829
|
+
*/
|
|
830
|
+
export declare function revokeApiKey(keyId: string): Promise<void>;
|
|
648
831
|
export {};
|
package/dist/lib/api.js
CHANGED
|
@@ -1162,3 +1162,211 @@ export async function getSafeHouseHarnessState() {
|
|
|
1162
1162
|
}
|
|
1163
1163
|
return (await response.json());
|
|
1164
1164
|
}
|
|
1165
|
+
function buildSignalListQuery(opts = {}) {
|
|
1166
|
+
const params = new URLSearchParams();
|
|
1167
|
+
if (opts.source)
|
|
1168
|
+
params.set("source", opts.source);
|
|
1169
|
+
if (opts.severity)
|
|
1170
|
+
params.set("severity", opts.severity);
|
|
1171
|
+
if (opts.status)
|
|
1172
|
+
params.set("status", opts.status);
|
|
1173
|
+
if (opts.scope)
|
|
1174
|
+
params.set("scope", opts.scope);
|
|
1175
|
+
if (opts.pattern_type)
|
|
1176
|
+
params.set("pattern_type", opts.pattern_type);
|
|
1177
|
+
if (opts.since)
|
|
1178
|
+
params.set("since", opts.since);
|
|
1179
|
+
if (opts.limit)
|
|
1180
|
+
params.set("limit", String(opts.limit));
|
|
1181
|
+
const qs = params.toString();
|
|
1182
|
+
return qs ? `?${qs}` : "";
|
|
1183
|
+
}
|
|
1184
|
+
async function gFetch(path, init = {}, notFoundLabel) {
|
|
1185
|
+
const url = validateUrl(`${API_BASE}${path}`);
|
|
1186
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1187
|
+
method: init.method ?? "GET",
|
|
1188
|
+
headers: {
|
|
1189
|
+
...(await authHeaders()),
|
|
1190
|
+
Accept: "application/json",
|
|
1191
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
1192
|
+
...(init.headers ?? {}),
|
|
1193
|
+
},
|
|
1194
|
+
body: init.body,
|
|
1195
|
+
}));
|
|
1196
|
+
if (!response.ok) {
|
|
1197
|
+
if (response.status === 401)
|
|
1198
|
+
throw new Error("Not authenticated.");
|
|
1199
|
+
if (response.status === 403)
|
|
1200
|
+
throw new Error("Permission denied (need org admin / membership).");
|
|
1201
|
+
if (response.status === 404 && notFoundLabel)
|
|
1202
|
+
throw new Error(`${notFoundLabel} not found.`);
|
|
1203
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1204
|
+
throw new Error(err.message || `Request failed: ${response.status}`);
|
|
1205
|
+
}
|
|
1206
|
+
return (await response.json());
|
|
1207
|
+
}
|
|
1208
|
+
export async function listGovernanceSignalsForOrg(orgId, opts = {}) {
|
|
1209
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Org");
|
|
1210
|
+
}
|
|
1211
|
+
export async function listGovernanceSignalsForTeam(teamId, opts = {}) {
|
|
1212
|
+
return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Team");
|
|
1213
|
+
}
|
|
1214
|
+
export async function listGovernanceSignalsForAgent(agentId, opts = {}) {
|
|
1215
|
+
return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Agent");
|
|
1216
|
+
}
|
|
1217
|
+
export async function getGovernanceSignal(id) {
|
|
1218
|
+
return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, "Signal");
|
|
1219
|
+
}
|
|
1220
|
+
export async function acknowledgeGovernanceSignal(id, body = {}) {
|
|
1221
|
+
return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, "Signal");
|
|
1222
|
+
}
|
|
1223
|
+
export async function resolveGovernanceSignal(id, body) {
|
|
1224
|
+
return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, "Signal");
|
|
1225
|
+
}
|
|
1226
|
+
export async function dismissGovernanceSignal(id, body = {}) {
|
|
1227
|
+
return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, "Signal");
|
|
1228
|
+
}
|
|
1229
|
+
export async function listGovernanceDestinations(orgId) {
|
|
1230
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, "Org");
|
|
1231
|
+
}
|
|
1232
|
+
export async function createGovernanceDestination(orgId, body) {
|
|
1233
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, "Org");
|
|
1234
|
+
}
|
|
1235
|
+
export async function deleteGovernanceDestination(orgId, destinationId) {
|
|
1236
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, "Destination");
|
|
1237
|
+
}
|
|
1238
|
+
export async function testGovernanceDestination(orgId, destinationId) {
|
|
1239
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, "Destination");
|
|
1240
|
+
}
|
|
1241
|
+
export async function listGovernanceRules(orgId) {
|
|
1242
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, "Org");
|
|
1243
|
+
}
|
|
1244
|
+
export async function createGovernanceRule(orgId, body) {
|
|
1245
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, "Org");
|
|
1246
|
+
}
|
|
1247
|
+
export async function deleteGovernanceRule(orgId, ruleId) {
|
|
1248
|
+
return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, "Rule");
|
|
1249
|
+
}
|
|
1250
|
+
// ─── api-keys (ADR-049) ─────────────────────────────────────────────────
|
|
1251
|
+
/**
|
|
1252
|
+
* Capability-based scope vocabulary (ADR-049). Mirrors the API's
|
|
1253
|
+
* VALID_SCOPES exactly. Update both surfaces together when the
|
|
1254
|
+
* vocabulary changes.
|
|
1255
|
+
*/
|
|
1256
|
+
export const API_KEY_SCOPES = [
|
|
1257
|
+
"gateway",
|
|
1258
|
+
"api:read",
|
|
1259
|
+
"api:write",
|
|
1260
|
+
"admin:org",
|
|
1261
|
+
"admin:platform",
|
|
1262
|
+
];
|
|
1263
|
+
export const DEFAULT_API_KEY_SCOPES = [
|
|
1264
|
+
"gateway",
|
|
1265
|
+
"api:read",
|
|
1266
|
+
"api:write",
|
|
1267
|
+
];
|
|
1268
|
+
/**
|
|
1269
|
+
* Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
|
|
1270
|
+
* keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
|
|
1271
|
+
* for display purposes; the auth gate handles the actual aliasing.
|
|
1272
|
+
*/
|
|
1273
|
+
export function isLegacyScopeSet(scopes) {
|
|
1274
|
+
if (!scopes || scopes.length === 0)
|
|
1275
|
+
return false;
|
|
1276
|
+
if (scopes.length === 2 && scopes.includes("gateway") && scopes.includes("api")) {
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
if (scopes.length === 1 && scopes[0] === "api")
|
|
1280
|
+
return true;
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* GET /v1/api-keys — list the caller's active personal API keys.
|
|
1285
|
+
*/
|
|
1286
|
+
export async function listApiKeys() {
|
|
1287
|
+
const url = validateUrl(`${API_BASE}/v1/api-keys`);
|
|
1288
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1289
|
+
headers: await authHeaders(),
|
|
1290
|
+
}));
|
|
1291
|
+
if (!response.ok) {
|
|
1292
|
+
if (response.status === 401) {
|
|
1293
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1294
|
+
}
|
|
1295
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1296
|
+
throw new Error(err.message || `Failed to list api keys: ${response.status}`);
|
|
1297
|
+
}
|
|
1298
|
+
const data = (await response.json());
|
|
1299
|
+
return data.keys ?? [];
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* POST /v1/api-keys — mint a new personal API key with explicit scopes.
|
|
1303
|
+
*
|
|
1304
|
+
* Returns the full secret only on this call. The mint-time ceiling
|
|
1305
|
+
* rejects admin scopes the caller is not eligible for (admin:platform
|
|
1306
|
+
* for non-staff, admin:org for users not in any org-admin role).
|
|
1307
|
+
*/
|
|
1308
|
+
export async function createApiKey(name, scopes) {
|
|
1309
|
+
const url = validateUrl(`${API_BASE}/v1/api-keys`);
|
|
1310
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1311
|
+
method: "POST",
|
|
1312
|
+
headers: { ...(await authHeaders()), "Content-Type": "application/json" },
|
|
1313
|
+
body: JSON.stringify({ name, scopes }),
|
|
1314
|
+
}));
|
|
1315
|
+
if (!response.ok) {
|
|
1316
|
+
if (response.status === 401) {
|
|
1317
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1318
|
+
}
|
|
1319
|
+
const body = await response.text().catch(() => "");
|
|
1320
|
+
if (response.status === 403) {
|
|
1321
|
+
throw new Error(`Mint-time ceiling rejected scope: ${body}`);
|
|
1322
|
+
}
|
|
1323
|
+
if (response.status === 400) {
|
|
1324
|
+
throw new Error(`Invalid scope(s): ${body}`);
|
|
1325
|
+
}
|
|
1326
|
+
throw new Error(`Failed to create api key: ${response.status} ${body}`);
|
|
1327
|
+
}
|
|
1328
|
+
return (await response.json());
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* POST /v1/api-keys/{key_id}/rotate — atomic mint-new + revoke-old.
|
|
1332
|
+
* Returns the full new secret only on this call. The new key inherits
|
|
1333
|
+
* the old key's name and scopes verbatim; the old key is revoked the
|
|
1334
|
+
* moment this returns.
|
|
1335
|
+
*/
|
|
1336
|
+
export async function rotateApiKey(keyId) {
|
|
1337
|
+
const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}/rotate`);
|
|
1338
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1339
|
+
method: "POST",
|
|
1340
|
+
headers: { ...(await authHeaders()), "Content-Type": "application/json" },
|
|
1341
|
+
}));
|
|
1342
|
+
if (!response.ok) {
|
|
1343
|
+
if (response.status === 401) {
|
|
1344
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1345
|
+
}
|
|
1346
|
+
if (response.status === 404)
|
|
1347
|
+
throw new Error(`Key not found: ${keyId}`);
|
|
1348
|
+
const body = await response.text().catch(() => "");
|
|
1349
|
+
throw new Error(`Failed to rotate api key: ${response.status} ${body}`);
|
|
1350
|
+
}
|
|
1351
|
+
return (await response.json());
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
|
|
1355
|
+
* audit; `is_active` flips to false and `revoked_at` is timestamped.
|
|
1356
|
+
*/
|
|
1357
|
+
export async function revokeApiKey(keyId) {
|
|
1358
|
+
const url = validateUrl(`${API_BASE}/v1/api-keys/${encodeURIComponent(keyId)}`);
|
|
1359
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1360
|
+
method: "DELETE",
|
|
1361
|
+
headers: await authHeaders(),
|
|
1362
|
+
}));
|
|
1363
|
+
if (!response.ok) {
|
|
1364
|
+
if (response.status === 401) {
|
|
1365
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1366
|
+
}
|
|
1367
|
+
if (response.status === 404)
|
|
1368
|
+
throw new Error(`Key not found: ${keyId}`);
|
|
1369
|
+
const body = await response.text().catch(() => "");
|
|
1370
|
+
throw new Error(`Failed to revoke api key: ${response.status} ${body}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
package/dist/lib/format.d.ts
CHANGED
package/dist/lib/format.js
CHANGED