@mnemom/mnemom 0.12.1 → 0.13.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/card.d.ts +4 -1
- package/dist/commands/card.js +129 -16
- package/dist/commands/integrity.js +7 -3
- package/dist/commands/license.js +30 -42
- package/dist/commands/logs.js +25 -9
- package/dist/commands/protection.d.ts +4 -1
- package/dist/commands/protection.js +74 -2
- package/dist/commands/status.js +12 -4
- package/dist/index.js +12 -7
- package/dist/lib/api.d.ts +214 -110
- package/dist/lib/api.js +305 -287
- package/dist/lib/webhooks-api.js +10 -5
- package/dist/version.d.ts +1 -0
- package/dist/version.js +16 -0
- package/package.json +5 -2
package/dist/lib/api.js
CHANGED
|
@@ -26,15 +26,95 @@ function validateUrl(url) {
|
|
|
26
26
|
export function newIdempotencyKey() {
|
|
27
27
|
return randomUUID();
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract the human message + code + details from a raw error body,
|
|
31
|
+
* tolerating all three live envelope shapes (prefer nested).
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeApiError(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object")
|
|
35
|
+
return {};
|
|
36
|
+
let message;
|
|
37
|
+
let code;
|
|
38
|
+
let details;
|
|
39
|
+
const e = raw.error;
|
|
40
|
+
if (e && typeof e === "object") {
|
|
41
|
+
// Nested {error:{code,message,details}} (enforce-hook).
|
|
42
|
+
message = typeof e.message === "string" ? e.message : undefined;
|
|
43
|
+
code = typeof e.code === "string" ? e.code : undefined;
|
|
44
|
+
details = e.details;
|
|
45
|
+
}
|
|
46
|
+
else if (typeof e === "string") {
|
|
47
|
+
// Flat {error:"message"} (+ optional top-level code on auth routes).
|
|
48
|
+
message = e;
|
|
49
|
+
}
|
|
50
|
+
// Fallbacks: top-level message/code/details when the above didn't populate.
|
|
51
|
+
if (!message && typeof raw.message === "string")
|
|
52
|
+
message = raw.message;
|
|
53
|
+
if (!code && typeof raw.code === "string")
|
|
54
|
+
code = raw.code;
|
|
55
|
+
if (details === undefined)
|
|
56
|
+
details = raw.details;
|
|
57
|
+
const spec_deviation = raw.spec_deviation && typeof raw.spec_deviation === "object" ? raw.spec_deviation : undefined;
|
|
58
|
+
return { message, code, details, conflict_agent_id: raw.conflict_agent_id, spec_deviation };
|
|
59
|
+
}
|
|
60
|
+
/** Parse a non-OK Response body into normalized error fields (never throws). */
|
|
61
|
+
export async function parseApiErrorBody(response) {
|
|
62
|
+
const raw = (await response.json().catch(() => null));
|
|
63
|
+
return normalizeApiError(raw);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Structured API error. `message` is the human-readable server message (or a
|
|
67
|
+
* fallback); `.status`/`.code`/`.details`/`.conflictAgentId` give callers a
|
|
68
|
+
* structured surface to branch on without parsing the message string.
|
|
69
|
+
* `instanceof MnemomApiError` narrows in command-layer catch blocks.
|
|
70
|
+
*/
|
|
71
|
+
export class MnemomApiError extends Error {
|
|
72
|
+
/** The wire HTTP status — may be a synthetic 500 from the enforce hook. */
|
|
73
|
+
status;
|
|
74
|
+
/**
|
|
75
|
+
* The TRUE intended status: `spec_deviation.original_status ?? status`. The
|
|
76
|
+
* enforce hook can rewrite an undocumented status to a synthetic 500; command
|
|
77
|
+
* layers MUST branch on `effectiveStatus` (not `status`) for 404/empty-state
|
|
78
|
+
* detection or they'll be fooled by the rewrite.
|
|
79
|
+
*/
|
|
80
|
+
effectiveStatus;
|
|
81
|
+
code;
|
|
82
|
+
details;
|
|
83
|
+
conflictAgentId;
|
|
84
|
+
/** Top-level enforce-hook sibling (present only on a spec-rewritten body). */
|
|
85
|
+
specDeviation;
|
|
86
|
+
constructor(status, message, opts = {}) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = "MnemomApiError";
|
|
89
|
+
this.status = status;
|
|
90
|
+
this.code = opts.code;
|
|
91
|
+
this.details = opts.details;
|
|
92
|
+
this.conflictAgentId = opts.conflictAgentId;
|
|
93
|
+
this.specDeviation = opts.specDeviation;
|
|
94
|
+
this.effectiveStatus = opts.specDeviation?.original_status ?? status;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read a non-OK Response into a MnemomApiError, preferring the server message
|
|
99
|
+
* and falling back to `${fallback}: ${status}` when the body carried none.
|
|
100
|
+
* Captures the enforce-hook `spec_deviation` sibling so `.effectiveStatus`
|
|
101
|
+
* reflects the true intended status behind any synthetic-500 rewrite.
|
|
102
|
+
*/
|
|
103
|
+
export async function readApiError(response, fallback) {
|
|
104
|
+
const parsed = await parseApiErrorBody(response);
|
|
105
|
+
const message = parsed.message || `${fallback}: ${response.status}`;
|
|
106
|
+
return new MnemomApiError(response.status, message, {
|
|
107
|
+
code: parsed.code,
|
|
108
|
+
details: parsed.details,
|
|
109
|
+
conflictAgentId: parsed.conflict_agent_id,
|
|
110
|
+
specDeviation: parsed.spec_deviation,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
29
113
|
async function fetchApi(endpoint) {
|
|
30
114
|
const url = validateUrl(`${API_BASE}${endpoint}`);
|
|
31
115
|
const response = await fetch(url);
|
|
32
116
|
if (!response.ok) {
|
|
33
|
-
|
|
34
|
-
error: "unknown",
|
|
35
|
-
message: response.statusText,
|
|
36
|
-
})));
|
|
37
|
-
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
117
|
+
throw await readApiError(response, "API request failed");
|
|
38
118
|
}
|
|
39
119
|
return response.json();
|
|
40
120
|
}
|
|
@@ -58,16 +138,22 @@ export async function postApi(endpoint, body, opts = {}) {
|
|
|
58
138
|
body: JSON.stringify(body),
|
|
59
139
|
});
|
|
60
140
|
if (!response.ok) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
141
|
+
const parsed = await parseApiErrorBody(response);
|
|
142
|
+
const msg = parsed.message || `HTTP ${response.status}`;
|
|
143
|
+
// Preserve the 409 conflict_agent_id surface: carry it structurally on the
|
|
144
|
+
// error (.conflictAgentId) AND keep it in the message for back-compat with
|
|
145
|
+
// any callers still string-matching the old "409: ... (conflict: ...)" form.
|
|
146
|
+
if (response.status === 409 && parsed.conflict_agent_id) {
|
|
147
|
+
throw new MnemomApiError(409, `409: ${msg} (conflict: ${parsed.conflict_agent_id})`, {
|
|
148
|
+
code: parsed.code,
|
|
149
|
+
details: parsed.details,
|
|
150
|
+
conflictAgentId: parsed.conflict_agent_id,
|
|
151
|
+
});
|
|
69
152
|
}
|
|
70
|
-
throw new
|
|
153
|
+
throw new MnemomApiError(response.status, `${response.status}: ${msg}`, {
|
|
154
|
+
code: parsed.code,
|
|
155
|
+
details: parsed.details,
|
|
156
|
+
});
|
|
71
157
|
}
|
|
72
158
|
return response.json();
|
|
73
159
|
}
|
|
@@ -126,10 +212,9 @@ export async function listAgents() {
|
|
|
126
212
|
}));
|
|
127
213
|
if (!response.ok) {
|
|
128
214
|
if (response.status === 401) {
|
|
129
|
-
throw new
|
|
215
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
130
216
|
}
|
|
131
|
-
|
|
132
|
-
throw new Error(err.message || `Failed to list agents: ${response.status}`);
|
|
217
|
+
throw await readApiError(response, "Failed to list agents");
|
|
133
218
|
}
|
|
134
219
|
const data = (await response.json());
|
|
135
220
|
return data.agents ?? [];
|
|
@@ -146,10 +231,9 @@ export async function listMyOrgs() {
|
|
|
146
231
|
}));
|
|
147
232
|
if (!response.ok) {
|
|
148
233
|
if (response.status === 401) {
|
|
149
|
-
throw new
|
|
234
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
150
235
|
}
|
|
151
|
-
|
|
152
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
236
|
+
throw await readApiError(response, "API request failed");
|
|
153
237
|
}
|
|
154
238
|
const data = (await response.json());
|
|
155
239
|
return data.orgs ?? [];
|
|
@@ -166,10 +250,9 @@ export async function getMyPersonalOrg() {
|
|
|
166
250
|
}));
|
|
167
251
|
if (!response.ok) {
|
|
168
252
|
if (response.status === 401) {
|
|
169
|
-
throw new
|
|
253
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
170
254
|
}
|
|
171
|
-
|
|
172
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
255
|
+
throw await readApiError(response, "API request failed");
|
|
173
256
|
}
|
|
174
257
|
return (await response.json());
|
|
175
258
|
}
|
|
@@ -211,13 +294,12 @@ export async function getTeam(teamId) {
|
|
|
211
294
|
}));
|
|
212
295
|
if (!response.ok) {
|
|
213
296
|
if (response.status === 401) {
|
|
214
|
-
throw new
|
|
297
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
215
298
|
}
|
|
216
299
|
if (response.status === 404) {
|
|
217
|
-
throw new
|
|
300
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found or not accessible.`);
|
|
218
301
|
}
|
|
219
|
-
|
|
220
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
302
|
+
throw await readApiError(response, "API request failed");
|
|
221
303
|
}
|
|
222
304
|
return (await response.json());
|
|
223
305
|
}
|
|
@@ -234,16 +316,15 @@ export async function getTeamTemplate(teamId, kind) {
|
|
|
234
316
|
}));
|
|
235
317
|
if (!response.ok) {
|
|
236
318
|
if (response.status === 401) {
|
|
237
|
-
throw new
|
|
319
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
238
320
|
}
|
|
239
321
|
if (response.status === 403) {
|
|
240
|
-
throw new
|
|
322
|
+
throw new MnemomApiError(403, `Forbidden: not a member of team '${teamId}'s org.`);
|
|
241
323
|
}
|
|
242
324
|
if (response.status === 404) {
|
|
243
|
-
throw new
|
|
325
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
244
326
|
}
|
|
245
|
-
|
|
246
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
327
|
+
throw await readApiError(response, "API request failed");
|
|
247
328
|
}
|
|
248
329
|
return (await response.json());
|
|
249
330
|
}
|
|
@@ -271,19 +352,18 @@ export async function putTeamTemplate(teamId, kind, yamlBody) {
|
|
|
271
352
|
}));
|
|
272
353
|
if (!response.ok) {
|
|
273
354
|
if (response.status === 401) {
|
|
274
|
-
throw new
|
|
355
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
275
356
|
}
|
|
276
357
|
if (response.status === 403) {
|
|
277
|
-
throw new
|
|
358
|
+
throw new MnemomApiError(403, `Forbidden: org admin or owner role required to write a team template.`);
|
|
278
359
|
}
|
|
279
360
|
if (response.status === 404) {
|
|
280
|
-
throw new
|
|
361
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
281
362
|
}
|
|
282
363
|
if (response.status === 413) {
|
|
283
|
-
throw new
|
|
364
|
+
throw new MnemomApiError(413, `Template too large (server limit: 128 KiB alignment / 64 KiB protection).`);
|
|
284
365
|
}
|
|
285
|
-
|
|
286
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
366
|
+
throw await readApiError(response, "API request failed");
|
|
287
367
|
}
|
|
288
368
|
return (await response.json());
|
|
289
369
|
}
|
|
@@ -305,16 +385,15 @@ export async function deleteTeamTemplate(teamId, kind) {
|
|
|
305
385
|
}));
|
|
306
386
|
if (!response.ok) {
|
|
307
387
|
if (response.status === 401) {
|
|
308
|
-
throw new
|
|
388
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
309
389
|
}
|
|
310
390
|
if (response.status === 403) {
|
|
311
|
-
throw new
|
|
391
|
+
throw new MnemomApiError(403, `Forbidden: org admin or owner role required to clear a team template.`);
|
|
312
392
|
}
|
|
313
393
|
if (response.status === 404) {
|
|
314
|
-
throw new
|
|
394
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
315
395
|
}
|
|
316
|
-
|
|
317
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
396
|
+
throw await readApiError(response, "API request failed");
|
|
318
397
|
}
|
|
319
398
|
return (await response.json());
|
|
320
399
|
}
|
|
@@ -337,8 +416,7 @@ export async function previewComposeTeamTemplate(teamId, kind, yamlBody) {
|
|
|
337
416
|
body: yamlBody,
|
|
338
417
|
}));
|
|
339
418
|
if (!response.ok) {
|
|
340
|
-
|
|
341
|
-
throw new Error(err.message || `Preview failed: ${response.status}`);
|
|
419
|
+
throw await readApiError(response, "Preview failed");
|
|
342
420
|
}
|
|
343
421
|
const body = (await response.json());
|
|
344
422
|
return { composed: body.composed, conflicts: body.conflicts };
|
|
@@ -365,16 +443,15 @@ export async function grantTeamAdmin(teamId, userId) {
|
|
|
365
443
|
}));
|
|
366
444
|
if (!response.ok) {
|
|
367
445
|
if (response.status === 401) {
|
|
368
|
-
throw new
|
|
446
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
369
447
|
}
|
|
370
448
|
if (response.status === 403) {
|
|
371
|
-
throw new
|
|
449
|
+
throw new MnemomApiError(403, `Forbidden: org owner or admin role required, and target user must be in the team's org.`);
|
|
372
450
|
}
|
|
373
451
|
if (response.status === 404) {
|
|
374
|
-
throw new
|
|
452
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
375
453
|
}
|
|
376
|
-
|
|
377
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
454
|
+
throw await readApiError(response, "API request failed");
|
|
378
455
|
}
|
|
379
456
|
return (await response.json());
|
|
380
457
|
}
|
|
@@ -398,16 +475,15 @@ export async function revokeTeamAdmin(teamId, userId) {
|
|
|
398
475
|
}));
|
|
399
476
|
if (!response.ok) {
|
|
400
477
|
if (response.status === 401) {
|
|
401
|
-
throw new
|
|
478
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
402
479
|
}
|
|
403
480
|
if (response.status === 403) {
|
|
404
|
-
throw new
|
|
481
|
+
throw new MnemomApiError(403, `Forbidden: revoke requires org owner/admin, or self-revoke with an active grant.`);
|
|
405
482
|
}
|
|
406
483
|
if (response.status === 404) {
|
|
407
|
-
throw new
|
|
484
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
408
485
|
}
|
|
409
|
-
|
|
410
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
486
|
+
throw await readApiError(response, "API request failed");
|
|
411
487
|
}
|
|
412
488
|
return (await response.json());
|
|
413
489
|
}
|
|
@@ -422,16 +498,15 @@ export async function listTeamAdmins(teamId) {
|
|
|
422
498
|
}));
|
|
423
499
|
if (!response.ok) {
|
|
424
500
|
if (response.status === 401) {
|
|
425
|
-
throw new
|
|
501
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
426
502
|
}
|
|
427
503
|
if (response.status === 403) {
|
|
428
|
-
throw new
|
|
504
|
+
throw new MnemomApiError(403, `Forbidden: requires membership in the team's org.`);
|
|
429
505
|
}
|
|
430
506
|
if (response.status === 404) {
|
|
431
|
-
throw new
|
|
507
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
432
508
|
}
|
|
433
|
-
|
|
434
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
509
|
+
throw await readApiError(response, "API request failed");
|
|
435
510
|
}
|
|
436
511
|
return (await response.json());
|
|
437
512
|
}
|
|
@@ -472,11 +547,7 @@ export async function getIntegrity(id) {
|
|
|
472
547
|
headers: await authHeaders(),
|
|
473
548
|
}));
|
|
474
549
|
if (!response.ok) {
|
|
475
|
-
|
|
476
|
-
error: "unknown",
|
|
477
|
-
message: response.statusText,
|
|
478
|
-
})));
|
|
479
|
-
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
550
|
+
throw await readApiError(response, "API request failed");
|
|
480
551
|
}
|
|
481
552
|
// Accept both the canonical docs shape (object) and the RPC-wrapped shape
|
|
482
553
|
// (array of one row). The latter is what prod actually returns today on
|
|
@@ -518,11 +589,7 @@ export async function getTraces(id, limit = 10) {
|
|
|
518
589
|
headers: await authHeaders(),
|
|
519
590
|
}));
|
|
520
591
|
if (!response.ok) {
|
|
521
|
-
|
|
522
|
-
error: "unknown",
|
|
523
|
-
message: response.statusText,
|
|
524
|
-
})));
|
|
525
|
-
throw new Error(error.message || `API request failed: ${response.status}`);
|
|
592
|
+
throw await readApiError(response, "API request failed");
|
|
526
593
|
}
|
|
527
594
|
const data = (await response.json());
|
|
528
595
|
// Accept both the envelope shape (current API) and a bare array (defensive
|
|
@@ -531,105 +598,6 @@ export async function getTraces(id, limit = 10) {
|
|
|
531
598
|
return data;
|
|
532
599
|
return data.traces ?? [];
|
|
533
600
|
}
|
|
534
|
-
export async function getCard(agentId) {
|
|
535
|
-
try {
|
|
536
|
-
return await fetchApi(`/v1/agents/${agentId}/card`);
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
540
|
-
if (message.includes("404") || message.includes("not found")) {
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
throw error;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
export async function updateCard(agentId, cardJson, opts = {}) {
|
|
547
|
-
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/card`);
|
|
548
|
-
const response = await fetch(url, {
|
|
549
|
-
method: "PATCH",
|
|
550
|
-
headers: {
|
|
551
|
-
"Content-Type": "application/json",
|
|
552
|
-
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
553
|
-
...(await authHeaders()),
|
|
554
|
-
},
|
|
555
|
-
body: sanitizeForHttp(JSON.stringify({ card_json: cardJson })),
|
|
556
|
-
});
|
|
557
|
-
if (!response.ok) {
|
|
558
|
-
const error = (await response.json().catch(() => ({
|
|
559
|
-
error: "unknown",
|
|
560
|
-
message: response.statusText,
|
|
561
|
-
})));
|
|
562
|
-
throw new Error(error.message || `Card update failed: ${response.status}`);
|
|
563
|
-
}
|
|
564
|
-
return response.json();
|
|
565
|
-
}
|
|
566
|
-
export async function reverifyAgent(agentId, opts = {}) {
|
|
567
|
-
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/reverify`);
|
|
568
|
-
const response = await fetch(url, {
|
|
569
|
-
method: "POST",
|
|
570
|
-
headers: {
|
|
571
|
-
"Content-Type": "application/json",
|
|
572
|
-
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
573
|
-
...(await authHeaders()),
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
if (!response.ok) {
|
|
577
|
-
const error = (await response.json().catch(() => ({
|
|
578
|
-
error: "unknown",
|
|
579
|
-
message: response.statusText,
|
|
580
|
-
})));
|
|
581
|
-
throw new Error(error.message || `Reverify failed: ${response.status}`);
|
|
582
|
-
}
|
|
583
|
-
return response.json();
|
|
584
|
-
}
|
|
585
|
-
export async function getPolicy(agentId) {
|
|
586
|
-
try {
|
|
587
|
-
return await fetchApi(`/v1/agents/${agentId}/policy`);
|
|
588
|
-
}
|
|
589
|
-
catch (error) {
|
|
590
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
591
|
-
if (message.includes("404") || message.includes("not found")) {
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
throw error;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
export async function publishPolicy(agentId, policyJson, opts = {}) {
|
|
598
|
-
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/policy`);
|
|
599
|
-
const response = await fetch(url, {
|
|
600
|
-
method: "PUT",
|
|
601
|
-
headers: {
|
|
602
|
-
"Content-Type": "application/json",
|
|
603
|
-
"Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
|
|
604
|
-
...(await authHeaders()),
|
|
605
|
-
},
|
|
606
|
-
body: sanitizeForHttp(JSON.stringify({ policy_json: policyJson })),
|
|
607
|
-
});
|
|
608
|
-
if (!response.ok) {
|
|
609
|
-
const error = (await response.json().catch(() => ({
|
|
610
|
-
error: "unknown",
|
|
611
|
-
message: response.statusText,
|
|
612
|
-
})));
|
|
613
|
-
throw new Error(error.message || `Policy publish failed: ${response.status}`);
|
|
614
|
-
}
|
|
615
|
-
return response.json();
|
|
616
|
-
}
|
|
617
|
-
export async function testPolicyHistorical(agentId, policyJson, limit = 50) {
|
|
618
|
-
const url = validateUrl(`${API_BASE}/v1/policies/evaluate/historical`);
|
|
619
|
-
const response = await fetch(url, {
|
|
620
|
-
method: "POST",
|
|
621
|
-
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
|
|
622
|
-
body: sanitizeForHttp(JSON.stringify({ agent_id: agentId, policy_json: policyJson, limit })),
|
|
623
|
-
});
|
|
624
|
-
if (!response.ok) {
|
|
625
|
-
const error = (await response.json().catch(() => ({
|
|
626
|
-
error: "unknown",
|
|
627
|
-
message: response.statusText,
|
|
628
|
-
})));
|
|
629
|
-
throw new Error(error.message || `Historical evaluation failed: ${response.status}`);
|
|
630
|
-
}
|
|
631
|
-
return response.json();
|
|
632
|
-
}
|
|
633
601
|
// ============================================================================
|
|
634
602
|
// Unified Card API (UC-4+)
|
|
635
603
|
// ============================================================================
|
|
@@ -647,11 +615,7 @@ export async function getAlignmentCard(agentId, format = "yaml") {
|
|
|
647
615
|
if (response.status === 404) {
|
|
648
616
|
return { body: "", contentType: "" };
|
|
649
617
|
}
|
|
650
|
-
|
|
651
|
-
error: "unknown",
|
|
652
|
-
message: response.statusText,
|
|
653
|
-
})));
|
|
654
|
-
throw new Error(error.message || `Failed to fetch alignment card: ${response.status}`);
|
|
618
|
+
throw await readApiError(response, "Failed to fetch alignment card");
|
|
655
619
|
}
|
|
656
620
|
const ct = response.headers.get("content-type") ?? "";
|
|
657
621
|
const body = await response.text();
|
|
@@ -689,11 +653,7 @@ export async function putAlignmentCard(agentId, body, contentType = "text/yaml",
|
|
|
689
653
|
body: sanitizedBody,
|
|
690
654
|
}));
|
|
691
655
|
if (!response.ok) {
|
|
692
|
-
|
|
693
|
-
error: "unknown",
|
|
694
|
-
message: response.statusText,
|
|
695
|
-
})));
|
|
696
|
-
throw new Error(error.message || `Failed to publish alignment card: ${response.status}`);
|
|
656
|
+
throw await readApiError(response, "Failed to publish alignment card");
|
|
697
657
|
}
|
|
698
658
|
return response.json();
|
|
699
659
|
}
|
|
@@ -710,11 +670,7 @@ export async function getProtectionCard(agentId, format = "yaml") {
|
|
|
710
670
|
if (response.status === 404) {
|
|
711
671
|
return { body: "", contentType: "" };
|
|
712
672
|
}
|
|
713
|
-
|
|
714
|
-
error: "unknown",
|
|
715
|
-
message: response.statusText,
|
|
716
|
-
})));
|
|
717
|
-
throw new Error(error.message || `Failed to fetch protection card: ${response.status}`);
|
|
673
|
+
throw await readApiError(response, "Failed to fetch protection card");
|
|
718
674
|
}
|
|
719
675
|
const ct = response.headers.get("content-type") ?? "";
|
|
720
676
|
const body = await response.text();
|
|
@@ -744,14 +700,50 @@ export async function putProtectionCard(agentId, body, contentType = "text/yaml"
|
|
|
744
700
|
body: sanitizedBody,
|
|
745
701
|
}));
|
|
746
702
|
if (!response.ok) {
|
|
747
|
-
|
|
748
|
-
error: "unknown",
|
|
749
|
-
message: response.statusText,
|
|
750
|
-
})));
|
|
751
|
-
throw new Error(error.message || `Failed to publish protection card: ${response.status}`);
|
|
703
|
+
throw await readApiError(response, "Failed to publish protection card");
|
|
752
704
|
}
|
|
753
705
|
return response.json();
|
|
754
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Dry-run validate + compose a card body against the server's authoritative
|
|
709
|
+
* pipeline. Returns a structured result — a 422/400 validation failure is an
|
|
710
|
+
* expected `valid:false` outcome (NOT a thrown error). 401 throws (caller
|
|
711
|
+
* should fall back to offline validation); 403/5xx throw a MnemomApiError.
|
|
712
|
+
*/
|
|
713
|
+
export async function previewComposeAgentCard(agentId, kind, body, contentType = "text/yaml") {
|
|
714
|
+
const url = validateUrl(`${API_BASE}/v1/${kind}/agent/${encodeURIComponent(agentId)}/preview-compose`);
|
|
715
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: { ...(await authHeaders()), "Content-Type": contentType, Accept: "application/json" },
|
|
718
|
+
body: sanitizeForHttp(body),
|
|
719
|
+
}));
|
|
720
|
+
if (response.ok) {
|
|
721
|
+
const data = (await response.json());
|
|
722
|
+
return {
|
|
723
|
+
valid: true,
|
|
724
|
+
status: response.status,
|
|
725
|
+
composed: data.composed,
|
|
726
|
+
conflicts: data.conflicts ?? [],
|
|
727
|
+
coherence_violations: data.coherence_violations ?? [],
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// 401 → unauthenticated: signal the caller to fall back to offline validation.
|
|
731
|
+
if (response.status === 401) {
|
|
732
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
733
|
+
}
|
|
734
|
+
// 400 / 422 → the card is invalid: an expected validation outcome, not an
|
|
735
|
+
// exception. Surface the care-framed findings structurally.
|
|
736
|
+
if (response.status === 400 || response.status === 422) {
|
|
737
|
+
const parsed = await parseApiErrorBody(response);
|
|
738
|
+
return {
|
|
739
|
+
valid: false,
|
|
740
|
+
status: response.status,
|
|
741
|
+
error: { code: parsed.code, message: parsed.message, details: parsed.details },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
// 403 / 5xx → a genuine error (e.g. not the owner, server fault).
|
|
745
|
+
throw await readApiError(response, "Card validation (preview-compose) failed");
|
|
746
|
+
}
|
|
755
747
|
// ============================================================================
|
|
756
748
|
// Agent Resolution (server-side, no local config)
|
|
757
749
|
// ============================================================================
|
|
@@ -809,13 +801,12 @@ export async function listPostures(opts) {
|
|
|
809
801
|
}));
|
|
810
802
|
if (!response.ok) {
|
|
811
803
|
if (response.status === 401) {
|
|
812
|
-
throw new
|
|
804
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
813
805
|
}
|
|
814
806
|
if (response.status === 403) {
|
|
815
|
-
throw new
|
|
807
|
+
throw new MnemomApiError(403, `Forbidden: not a member of org '${opts.orgId}'.`);
|
|
816
808
|
}
|
|
817
|
-
|
|
818
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
809
|
+
throw await readApiError(response, "API request failed");
|
|
819
810
|
}
|
|
820
811
|
const data = (await response.json());
|
|
821
812
|
return data.postures ?? [];
|
|
@@ -828,14 +819,13 @@ export async function getPosture(postureId) {
|
|
|
828
819
|
}));
|
|
829
820
|
if (!response.ok) {
|
|
830
821
|
if (response.status === 401) {
|
|
831
|
-
throw new
|
|
822
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
832
823
|
}
|
|
833
824
|
if (response.status === 404)
|
|
834
|
-
throw new
|
|
825
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
835
826
|
if (response.status === 403)
|
|
836
|
-
throw new
|
|
837
|
-
|
|
838
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
827
|
+
throw new MnemomApiError(403, `Forbidden: not visible to your account.`);
|
|
828
|
+
throw await readApiError(response, "API request failed");
|
|
839
829
|
}
|
|
840
830
|
return (await response.json());
|
|
841
831
|
}
|
|
@@ -847,11 +837,10 @@ export async function listPostureRevisions(postureId) {
|
|
|
847
837
|
}));
|
|
848
838
|
if (!response.ok) {
|
|
849
839
|
if (response.status === 401)
|
|
850
|
-
throw new
|
|
840
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
851
841
|
if (response.status === 404)
|
|
852
|
-
throw new
|
|
853
|
-
|
|
854
|
-
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
842
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
843
|
+
throw await readApiError(response, "API request failed");
|
|
855
844
|
}
|
|
856
845
|
const data = (await response.json());
|
|
857
846
|
return data.revisions ?? [];
|
|
@@ -864,13 +853,16 @@ export async function diffPostureRevisions(postureId, fromNo, toNo) {
|
|
|
864
853
|
}));
|
|
865
854
|
if (!response.ok) {
|
|
866
855
|
if (response.status === 401)
|
|
867
|
-
throw new
|
|
856
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
868
857
|
if (response.status === 404) {
|
|
869
|
-
const errBody =
|
|
870
|
-
throw new
|
|
858
|
+
const errBody = await parseApiErrorBody(response);
|
|
859
|
+
throw new MnemomApiError(404, errBody.message || `Posture or revision not found.`, {
|
|
860
|
+
code: errBody.code,
|
|
861
|
+
details: errBody.details,
|
|
862
|
+
specDeviation: errBody.spec_deviation,
|
|
863
|
+
});
|
|
871
864
|
}
|
|
872
|
-
|
|
873
|
-
throw new Error(err.message || `Diff failed: ${response.status}`);
|
|
865
|
+
throw await readApiError(response, "Diff failed");
|
|
874
866
|
}
|
|
875
867
|
return (await response.json());
|
|
876
868
|
}
|
|
@@ -889,16 +881,15 @@ export async function createPosture(input) {
|
|
|
889
881
|
}));
|
|
890
882
|
if (!response.ok) {
|
|
891
883
|
if (response.status === 401)
|
|
892
|
-
throw new
|
|
884
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
893
885
|
if (response.status === 403) {
|
|
894
|
-
throw new
|
|
886
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin role required (or scope=platform reserved for migrations).`);
|
|
895
887
|
}
|
|
896
888
|
if (response.status === 409)
|
|
897
|
-
throw new
|
|
889
|
+
throw new MnemomApiError(409, `A posture with this slug already exists in the org.`);
|
|
898
890
|
if (response.status === 413)
|
|
899
|
-
throw new
|
|
900
|
-
|
|
901
|
-
throw new Error(err.message || `Create failed: ${response.status}`);
|
|
891
|
+
throw new MnemomApiError(413, `Posture body too large (server limit: 256 KiB).`);
|
|
892
|
+
throw await readApiError(response, "Create failed");
|
|
902
893
|
}
|
|
903
894
|
return (await response.json());
|
|
904
895
|
}
|
|
@@ -917,16 +908,15 @@ export async function updatePosture(postureId, input) {
|
|
|
917
908
|
}));
|
|
918
909
|
if (!response.ok) {
|
|
919
910
|
if (response.status === 401)
|
|
920
|
-
throw new
|
|
911
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
921
912
|
if (response.status === 403) {
|
|
922
|
-
throw new
|
|
913
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required, or this is a Mnemom-shipped platform default (immutable — clone first).`);
|
|
923
914
|
}
|
|
924
915
|
if (response.status === 404)
|
|
925
|
-
throw new
|
|
916
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
926
917
|
if (response.status === 413)
|
|
927
|
-
throw new
|
|
928
|
-
|
|
929
|
-
throw new Error(err.message || `Update failed: ${response.status}`);
|
|
918
|
+
throw new MnemomApiError(413, `Posture body too large (server limit: 256 KiB).`);
|
|
919
|
+
throw await readApiError(response, "Update failed");
|
|
930
920
|
}
|
|
931
921
|
return (await response.json());
|
|
932
922
|
}
|
|
@@ -945,15 +935,14 @@ export async function clonePosture(postureId, input) {
|
|
|
945
935
|
}));
|
|
946
936
|
if (!response.ok) {
|
|
947
937
|
if (response.status === 401)
|
|
948
|
-
throw new
|
|
938
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
949
939
|
if (response.status === 403)
|
|
950
|
-
throw new
|
|
940
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required on target org.`);
|
|
951
941
|
if (response.status === 404)
|
|
952
|
-
throw new
|
|
942
|
+
throw new MnemomApiError(404, `Source posture '${postureId}' not found.`);
|
|
953
943
|
if (response.status === 409)
|
|
954
|
-
throw new
|
|
955
|
-
|
|
956
|
-
throw new Error(err.message || `Clone failed: ${response.status}`);
|
|
944
|
+
throw new MnemomApiError(409, `A posture with this slug already exists in the target org.`);
|
|
945
|
+
throw await readApiError(response, "Clone failed");
|
|
957
946
|
}
|
|
958
947
|
return (await response.json());
|
|
959
948
|
}
|
|
@@ -970,16 +959,15 @@ export async function deletePosture(postureId) {
|
|
|
970
959
|
}));
|
|
971
960
|
if (!response.ok) {
|
|
972
961
|
if (response.status === 401)
|
|
973
|
-
throw new
|
|
962
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
974
963
|
if (response.status === 403)
|
|
975
|
-
throw new
|
|
964
|
+
throw new MnemomApiError(403, `Forbidden: platform defaults are immutable.`);
|
|
976
965
|
if (response.status === 404)
|
|
977
|
-
throw new
|
|
966
|
+
throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
|
|
978
967
|
if (response.status === 409) {
|
|
979
|
-
throw new
|
|
968
|
+
throw new MnemomApiError(409, `Cannot delete a posture that is currently assigned to one or more teams. Unassign first.`);
|
|
980
969
|
}
|
|
981
|
-
|
|
982
|
-
throw new Error(err.message || `Delete failed: ${response.status}`);
|
|
970
|
+
throw await readApiError(response, "Delete failed");
|
|
983
971
|
}
|
|
984
972
|
}
|
|
985
973
|
/** POST /v1/postures/:id/assign — assign to a team. */
|
|
@@ -997,15 +985,14 @@ export async function assignPosture(postureId, teamId, pinRevisionNo) {
|
|
|
997
985
|
}));
|
|
998
986
|
if (!response.ok) {
|
|
999
987
|
if (response.status === 401)
|
|
1000
|
-
throw new
|
|
988
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1001
989
|
if (response.status === 403)
|
|
1002
|
-
throw new
|
|
990
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required on team's org.`);
|
|
1003
991
|
if (response.status === 404) {
|
|
1004
|
-
const errBody =
|
|
1005
|
-
throw new
|
|
992
|
+
const errBody = await parseApiErrorBody(response);
|
|
993
|
+
throw new MnemomApiError(404, errBody.message || `Posture, team, or pinned revision not found.`, { code: errBody.code, details: errBody.details, specDeviation: errBody.spec_deviation });
|
|
1006
994
|
}
|
|
1007
|
-
|
|
1008
|
-
throw new Error(err.message || `Assign failed: ${response.status}`);
|
|
995
|
+
throw await readApiError(response, "Assign failed");
|
|
1009
996
|
}
|
|
1010
997
|
return (await response.json());
|
|
1011
998
|
}
|
|
@@ -1022,14 +1009,13 @@ export async function unassignPosture(postureId, teamId) {
|
|
|
1022
1009
|
}));
|
|
1023
1010
|
if (!response.ok) {
|
|
1024
1011
|
if (response.status === 401)
|
|
1025
|
-
throw new
|
|
1012
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1026
1013
|
if (response.status === 403)
|
|
1027
|
-
throw new
|
|
1014
|
+
throw new MnemomApiError(403, `Forbidden: org owner/admin required.`);
|
|
1028
1015
|
if (response.status === 404) {
|
|
1029
|
-
throw new
|
|
1016
|
+
throw new MnemomApiError(404, `No matching assignment to remove (posture '${postureId}' is not assigned to team '${teamId}').`);
|
|
1030
1017
|
}
|
|
1031
|
-
|
|
1032
|
-
throw new Error(err.message || `Unassign failed: ${response.status}`);
|
|
1018
|
+
throw await readApiError(response, "Unassign failed");
|
|
1033
1019
|
}
|
|
1034
1020
|
}
|
|
1035
1021
|
/** POST /v1/postures/:id/preview-compose — preview effective for a team. */
|
|
@@ -1046,13 +1032,16 @@ export async function previewComposePosture(postureId, teamId) {
|
|
|
1046
1032
|
}));
|
|
1047
1033
|
if (!response.ok) {
|
|
1048
1034
|
if (response.status === 401)
|
|
1049
|
-
throw new
|
|
1035
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1050
1036
|
if (response.status === 404) {
|
|
1051
|
-
const errBody =
|
|
1052
|
-
throw new
|
|
1037
|
+
const errBody = await parseApiErrorBody(response);
|
|
1038
|
+
throw new MnemomApiError(404, errBody.message || `Posture or team not found.`, {
|
|
1039
|
+
code: errBody.code,
|
|
1040
|
+
details: errBody.details,
|
|
1041
|
+
specDeviation: errBody.spec_deviation,
|
|
1042
|
+
});
|
|
1053
1043
|
}
|
|
1054
|
-
|
|
1055
|
-
throw new Error(err.message || `Preview-compose failed: ${response.status}`);
|
|
1044
|
+
throw await readApiError(response, "Preview-compose failed");
|
|
1056
1045
|
}
|
|
1057
1046
|
return (await response.json());
|
|
1058
1047
|
}
|
|
@@ -1076,13 +1065,12 @@ export async function listSidebandAdvisoriesForAgent(agentId, opts = {}) {
|
|
|
1076
1065
|
}));
|
|
1077
1066
|
if (!response.ok) {
|
|
1078
1067
|
if (response.status === 401)
|
|
1079
|
-
throw new
|
|
1068
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1080
1069
|
if (response.status === 403)
|
|
1081
|
-
throw new
|
|
1070
|
+
throw new MnemomApiError(403, "Permission denied (need org membership).");
|
|
1082
1071
|
if (response.status === 404)
|
|
1083
|
-
throw new
|
|
1084
|
-
|
|
1085
|
-
throw new Error(err.message || `List advisories failed: ${response.status}`);
|
|
1072
|
+
throw new MnemomApiError(404, `Agent '${agentId}' not found.`);
|
|
1073
|
+
throw await readApiError(response, "List advisories failed");
|
|
1086
1074
|
}
|
|
1087
1075
|
return (await response.json());
|
|
1088
1076
|
}
|
|
@@ -1106,13 +1094,12 @@ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
|
|
|
1106
1094
|
}));
|
|
1107
1095
|
if (!response.ok) {
|
|
1108
1096
|
if (response.status === 401)
|
|
1109
|
-
throw new
|
|
1097
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1110
1098
|
if (response.status === 403)
|
|
1111
|
-
throw new
|
|
1099
|
+
throw new MnemomApiError(403, "Permission denied (need org membership).");
|
|
1112
1100
|
if (response.status === 404)
|
|
1113
|
-
throw new
|
|
1114
|
-
|
|
1115
|
-
throw new Error(err.message || `List advisories failed: ${response.status}`);
|
|
1101
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
1102
|
+
throw await readApiError(response, "List advisories failed");
|
|
1116
1103
|
}
|
|
1117
1104
|
return (await response.json());
|
|
1118
1105
|
}
|
|
@@ -1131,13 +1118,12 @@ export async function getTeamSidebandCoverage(teamId) {
|
|
|
1131
1118
|
}));
|
|
1132
1119
|
if (!response.ok) {
|
|
1133
1120
|
if (response.status === 401)
|
|
1134
|
-
throw new
|
|
1121
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1135
1122
|
if (response.status === 403)
|
|
1136
|
-
throw new
|
|
1123
|
+
throw new MnemomApiError(403, "Permission denied (need org membership).");
|
|
1137
1124
|
if (response.status === 404)
|
|
1138
|
-
throw new
|
|
1139
|
-
|
|
1140
|
-
throw new Error(err.message || `Coverage fetch failed: ${response.status}`);
|
|
1125
|
+
throw new MnemomApiError(404, `Team '${teamId}' not found.`);
|
|
1126
|
+
throw await readApiError(response, "Coverage fetch failed");
|
|
1141
1127
|
}
|
|
1142
1128
|
return (await response.json());
|
|
1143
1129
|
}
|
|
@@ -1154,11 +1140,10 @@ export async function getSafeHouseHarnessState() {
|
|
|
1154
1140
|
}));
|
|
1155
1141
|
if (!response.ok) {
|
|
1156
1142
|
if (response.status === 401)
|
|
1157
|
-
throw new
|
|
1143
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login`.");
|
|
1158
1144
|
if (response.status === 403)
|
|
1159
|
-
throw new
|
|
1160
|
-
|
|
1161
|
-
throw new Error(err.message || `Harness state fetch failed: ${response.status}`);
|
|
1145
|
+
throw new MnemomApiError(403, "Permission denied — `mnemom validate safe-house` is mnemom-staff only.");
|
|
1146
|
+
throw await readApiError(response, "Harness state fetch failed");
|
|
1162
1147
|
}
|
|
1163
1148
|
return (await response.json());
|
|
1164
1149
|
}
|
|
@@ -1195,13 +1180,12 @@ async function gFetch(path, init = {}, notFoundLabel) {
|
|
|
1195
1180
|
}));
|
|
1196
1181
|
if (!response.ok) {
|
|
1197
1182
|
if (response.status === 401)
|
|
1198
|
-
throw new
|
|
1183
|
+
throw new MnemomApiError(401, "Not authenticated.");
|
|
1199
1184
|
if (response.status === 403)
|
|
1200
|
-
throw new
|
|
1185
|
+
throw new MnemomApiError(403, "Permission denied (need org admin / membership).");
|
|
1201
1186
|
if (response.status === 404 && notFoundLabel)
|
|
1202
|
-
throw new
|
|
1203
|
-
|
|
1204
|
-
throw new Error(err.message || `Request failed: ${response.status}`);
|
|
1187
|
+
throw new MnemomApiError(404, `${notFoundLabel} not found.`);
|
|
1188
|
+
throw await readApiError(response, "Request failed");
|
|
1205
1189
|
}
|
|
1206
1190
|
return (await response.json());
|
|
1207
1191
|
}
|
|
@@ -1286,10 +1270,9 @@ export async function listApiKeys() {
|
|
|
1286
1270
|
}));
|
|
1287
1271
|
if (!response.ok) {
|
|
1288
1272
|
if (response.status === 401) {
|
|
1289
|
-
throw new
|
|
1273
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1290
1274
|
}
|
|
1291
|
-
|
|
1292
|
-
throw new Error(err.message || `Failed to list api keys: ${response.status}`);
|
|
1275
|
+
throw await readApiError(response, "Failed to list api keys");
|
|
1293
1276
|
}
|
|
1294
1277
|
const data = (await response.json());
|
|
1295
1278
|
return data.keys ?? [];
|
|
@@ -1310,16 +1293,17 @@ export async function createApiKey(name, scopes) {
|
|
|
1310
1293
|
}));
|
|
1311
1294
|
if (!response.ok) {
|
|
1312
1295
|
if (response.status === 401) {
|
|
1313
|
-
throw new
|
|
1296
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1314
1297
|
}
|
|
1315
|
-
const
|
|
1298
|
+
const parsed = await parseApiErrorBody(response);
|
|
1299
|
+
const detail = parsed.message || `HTTP ${response.status}`;
|
|
1316
1300
|
if (response.status === 403) {
|
|
1317
|
-
throw new
|
|
1301
|
+
throw new MnemomApiError(403, `Mint-time ceiling rejected scope: ${detail}`);
|
|
1318
1302
|
}
|
|
1319
1303
|
if (response.status === 400) {
|
|
1320
|
-
throw new
|
|
1304
|
+
throw new MnemomApiError(400, `Invalid scope(s): ${detail}`);
|
|
1321
1305
|
}
|
|
1322
|
-
throw new
|
|
1306
|
+
throw new MnemomApiError(response.status, `Failed to create api key: ${response.status} ${detail}`);
|
|
1323
1307
|
}
|
|
1324
1308
|
return (await response.json());
|
|
1325
1309
|
}
|
|
@@ -1337,12 +1321,12 @@ export async function rotateApiKey(keyId) {
|
|
|
1337
1321
|
}));
|
|
1338
1322
|
if (!response.ok) {
|
|
1339
1323
|
if (response.status === 401) {
|
|
1340
|
-
throw new
|
|
1324
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1341
1325
|
}
|
|
1342
1326
|
if (response.status === 404)
|
|
1343
|
-
throw new
|
|
1344
|
-
const
|
|
1345
|
-
throw new
|
|
1327
|
+
throw new MnemomApiError(404, `Key not found: ${keyId}`);
|
|
1328
|
+
const parsed = await parseApiErrorBody(response);
|
|
1329
|
+
throw new MnemomApiError(response.status, `Failed to rotate api key: ${response.status} ${parsed.message ?? ""}`.trim());
|
|
1346
1330
|
}
|
|
1347
1331
|
return (await response.json());
|
|
1348
1332
|
}
|
|
@@ -1358,14 +1342,48 @@ export async function revokeApiKey(keyId) {
|
|
|
1358
1342
|
}));
|
|
1359
1343
|
if (!response.ok) {
|
|
1360
1344
|
if (response.status === 401) {
|
|
1361
|
-
throw new
|
|
1345
|
+
throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
1362
1346
|
}
|
|
1363
1347
|
if (response.status === 404)
|
|
1364
|
-
throw new
|
|
1365
|
-
const
|
|
1366
|
-
throw new
|
|
1348
|
+
throw new MnemomApiError(404, `Key not found: ${keyId}`);
|
|
1349
|
+
const parsed = await parseApiErrorBody(response);
|
|
1350
|
+
throw new MnemomApiError(response.status, `Failed to revoke api key: ${response.status} ${parsed.message ?? ""}`.trim());
|
|
1367
1351
|
}
|
|
1368
1352
|
}
|
|
1353
|
+
/**
|
|
1354
|
+
* POST /v1/license/validate — validate a license JWT for an instance.
|
|
1355
|
+
*
|
|
1356
|
+
* UNAUTHENTICATED by design (the license JWT IS the credential; this is a
|
|
1357
|
+
* pre-login path) — sends NO auth header. Source-verified at canonical ref
|
|
1358
|
+
* c18491e6 (handleLicenseValidate): the request body keys are
|
|
1359
|
+
* `{ license, instance_id, instance_metadata }` (NOT `jwt`) — sending `jwt`
|
|
1360
|
+
* would 400 "license is required".
|
|
1361
|
+
*
|
|
1362
|
+
* Returns the parsed validation result on 2xx (including the grace-period
|
|
1363
|
+
* `valid:false` 200 body); throws MnemomApiError on non-2xx so the command
|
|
1364
|
+
* renders `.message`/`.status`. This fixes the live H1 "[object Object]" bug:
|
|
1365
|
+
* the old command-layer `err.error || "unknown"` string-coerced the nested
|
|
1366
|
+
* `{code,message}` object that the enforce hook puts on the wire.
|
|
1367
|
+
*
|
|
1368
|
+
* `license deactivate` may reuse this (best-effort, fire-and-forget) by
|
|
1369
|
+
* catching the throw at the command layer.
|
|
1370
|
+
*/
|
|
1371
|
+
export async function validateLicense(jwt, instanceId, instanceMetadata = {}) {
|
|
1372
|
+
const url = validateUrl(`${API_BASE}/v1/license/validate`);
|
|
1373
|
+
const response = await fetch(url, {
|
|
1374
|
+
method: "POST",
|
|
1375
|
+
headers: { "Content-Type": "application/json" },
|
|
1376
|
+
body: JSON.stringify({
|
|
1377
|
+
license: sanitizeForHttp(jwt),
|
|
1378
|
+
instance_id: instanceId,
|
|
1379
|
+
instance_metadata: instanceMetadata,
|
|
1380
|
+
}),
|
|
1381
|
+
});
|
|
1382
|
+
if (!response.ok) {
|
|
1383
|
+
throw await readApiError(response, "License validation failed");
|
|
1384
|
+
}
|
|
1385
|
+
return (await response.json());
|
|
1386
|
+
}
|
|
1369
1387
|
export async function reportRecipeFnFp(recipeId, input) {
|
|
1370
1388
|
return postApi(`/v1/recipes/${encodeURIComponent(recipeId)}/report`, input);
|
|
1371
1389
|
}
|