@mnemom/mnemom 0.9.1 → 0.10.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/advisories.d.ts +32 -0
- package/dist/commands/advisories.js +158 -0
- package/dist/commands/org.d.ts +23 -0
- package/dist/commands/org.js +122 -0
- package/dist/commands/posture.d.ts +71 -0
- package/dist/commands/posture.js +440 -0
- package/dist/commands/team.d.ts +56 -0
- package/dist/commands/team.js +507 -0
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +150 -0
- package/dist/index.js +442 -14
- package/dist/lib/api.d.ts +393 -0
- package/dist/lib/api.js +676 -5
- package/package.json +1 -1
package/dist/lib/api.js
CHANGED
|
@@ -52,6 +52,7 @@ export async function postApi(endpoint, body, opts = {}) {
|
|
|
52
52
|
headers["X-Mnemom-Api-Key"] = cred.key;
|
|
53
53
|
}
|
|
54
54
|
const response = await fetch(url, {
|
|
55
|
+
// lgtm[js/file-data-url]
|
|
55
56
|
method: "POST",
|
|
56
57
|
headers,
|
|
57
58
|
body: JSON.stringify(body),
|
|
@@ -127,12 +128,313 @@ export async function listAgents() {
|
|
|
127
128
|
if (response.status === 401) {
|
|
128
129
|
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
129
130
|
}
|
|
130
|
-
const err = await response.json().catch(() => ({ error: "unknown" }));
|
|
131
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
131
132
|
throw new Error(err.message || `Failed to list agents: ${response.status}`);
|
|
132
133
|
}
|
|
133
|
-
const data = await response.json();
|
|
134
|
+
const data = (await response.json());
|
|
134
135
|
return data.agents ?? [];
|
|
135
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* GET /v1/orgs — list every org the user is a member of, including the
|
|
139
|
+
* personal-org-of-one (per ADR-044 Option A, GitHub model). Personal sorts
|
|
140
|
+
* first; multi-user orgs follow.
|
|
141
|
+
*/
|
|
142
|
+
export async function listMyOrgs() {
|
|
143
|
+
const url = validateUrl(`${API_BASE}/v1/orgs`);
|
|
144
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
145
|
+
headers: await authHeaders(),
|
|
146
|
+
}));
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
if (response.status === 401) {
|
|
149
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
150
|
+
}
|
|
151
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
152
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
153
|
+
}
|
|
154
|
+
const data = (await response.json());
|
|
155
|
+
return data.orgs ?? [];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* GET /v1/auth/me/personal-org — accessor for the user's personal org.
|
|
159
|
+
* Idempotent: lazily provisions for legacy accounts that pre-date the
|
|
160
|
+
* mnemom-api migration 161 backfill.
|
|
161
|
+
*/
|
|
162
|
+
export async function getMyPersonalOrg() {
|
|
163
|
+
const url = validateUrl(`${API_BASE}/v1/auth/me/personal-org`);
|
|
164
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
165
|
+
headers: await authHeaders(),
|
|
166
|
+
}));
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
if (response.status === 401) {
|
|
169
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
170
|
+
}
|
|
171
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
172
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
173
|
+
}
|
|
174
|
+
return (await response.json());
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* GET /v1/orgs/:org_id/teams for every org the user is a member of,
|
|
178
|
+
* concatenated. The CLI consumer that just wants "every team I can see"
|
|
179
|
+
* doesn't need to know about org boundaries; team_id is unique anyway.
|
|
180
|
+
* Returns the flat list with org_name attached for display.
|
|
181
|
+
*/
|
|
182
|
+
export async function listMyTeams() {
|
|
183
|
+
const orgs = await listMyOrgs();
|
|
184
|
+
const teams = [];
|
|
185
|
+
for (const org of orgs) {
|
|
186
|
+
const url = validateUrl(`${API_BASE}/v1/orgs/${org.org_id}/teams`);
|
|
187
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
188
|
+
headers: await authHeaders(),
|
|
189
|
+
}));
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
// Skip orgs the user can't read teams from (e.g., role doesn't permit).
|
|
192
|
+
// The list call should succeed at the member level, but tolerate edge cases.
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const data = (await response.json());
|
|
196
|
+
for (const t of data.teams ?? []) {
|
|
197
|
+
teams.push({ ...t, org_id: t.org_id ?? org.org_id, org_name: org.name });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return teams;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* GET /v1/teams/:team_id — fetch a single team's row. Returns the team
|
|
204
|
+
* detail including org_id (which the CLI uses to disambiguate team_id
|
|
205
|
+
* collisions across orgs in error messages).
|
|
206
|
+
*/
|
|
207
|
+
export async function getTeam(teamId) {
|
|
208
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}`);
|
|
209
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
210
|
+
headers: await authHeaders(),
|
|
211
|
+
}));
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
if (response.status === 401) {
|
|
214
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
215
|
+
}
|
|
216
|
+
if (response.status === 404) {
|
|
217
|
+
throw new Error(`Team '${teamId}' not found or not accessible.`);
|
|
218
|
+
}
|
|
219
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
220
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
221
|
+
}
|
|
222
|
+
return (await response.json());
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* GET /v1/teams/:team_id/(alignment|protection)-template — read the
|
|
226
|
+
* current team-scope template + its enabled flag. Returns the wire body
|
|
227
|
+
* verbatim. Empty for teams that have no template set yet (template:
|
|
228
|
+
* null, enabled: false).
|
|
229
|
+
*/
|
|
230
|
+
export async function getTeamTemplate(teamId, kind) {
|
|
231
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template`);
|
|
232
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
233
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
234
|
+
}));
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
if (response.status === 401) {
|
|
237
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
238
|
+
}
|
|
239
|
+
if (response.status === 403) {
|
|
240
|
+
throw new Error(`Forbidden: not a member of team '${teamId}'s org.`);
|
|
241
|
+
}
|
|
242
|
+
if (response.status === 404) {
|
|
243
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
244
|
+
}
|
|
245
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
246
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
247
|
+
}
|
|
248
|
+
return (await response.json());
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* PUT /v1/teams/:team_id/(alignment|protection)-template — write the
|
|
252
|
+
* team-scope template. Body is YAML or JSON template content; the API
|
|
253
|
+
* accepts either via Content-Type. Idempotency-Key is required (per the
|
|
254
|
+
* idempotency-with-body-hash discipline in the API).
|
|
255
|
+
*
|
|
256
|
+
* On success returns the post-write team template body, including
|
|
257
|
+
* agents_flagged_for_recompose so the caller can surface the fan-out
|
|
258
|
+
* count to the user.
|
|
259
|
+
*/
|
|
260
|
+
export async function putTeamTemplate(teamId, kind, yamlBody) {
|
|
261
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template`);
|
|
262
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
263
|
+
method: "PUT",
|
|
264
|
+
headers: {
|
|
265
|
+
...(await authHeaders()),
|
|
266
|
+
"Content-Type": "text/yaml",
|
|
267
|
+
Accept: "application/json",
|
|
268
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
269
|
+
},
|
|
270
|
+
body: yamlBody,
|
|
271
|
+
}));
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
if (response.status === 401) {
|
|
274
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
275
|
+
}
|
|
276
|
+
if (response.status === 403) {
|
|
277
|
+
throw new Error(`Forbidden: org admin or owner role required to write a team template.`);
|
|
278
|
+
}
|
|
279
|
+
if (response.status === 404) {
|
|
280
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
281
|
+
}
|
|
282
|
+
if (response.status === 413) {
|
|
283
|
+
throw new Error(`Template too large (server limit: 128 KiB alignment / 64 KiB protection).`);
|
|
284
|
+
}
|
|
285
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
286
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
287
|
+
}
|
|
288
|
+
return (await response.json());
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* DELETE /v1/teams/:team_id/(alignment|protection)-template — clear the
|
|
292
|
+
* template. Idempotent (deleting an already-cleared template is a 200).
|
|
293
|
+
* Returns the post-delete team template body with deleted=true and the
|
|
294
|
+
* recompose fan-out count.
|
|
295
|
+
*/
|
|
296
|
+
export async function deleteTeamTemplate(teamId, kind) {
|
|
297
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template`);
|
|
298
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
299
|
+
method: "DELETE",
|
|
300
|
+
headers: {
|
|
301
|
+
...(await authHeaders()),
|
|
302
|
+
Accept: "application/json",
|
|
303
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
304
|
+
},
|
|
305
|
+
}));
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
if (response.status === 401) {
|
|
308
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
309
|
+
}
|
|
310
|
+
if (response.status === 403) {
|
|
311
|
+
throw new Error(`Forbidden: org admin or owner role required to clear a team template.`);
|
|
312
|
+
}
|
|
313
|
+
if (response.status === 404) {
|
|
314
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
315
|
+
}
|
|
316
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
317
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
318
|
+
}
|
|
319
|
+
return (await response.json());
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* POST /v1/teams/:team_id/(alignment|protection)-template/preview-compose
|
|
323
|
+
* — dry-run the composer with the supplied draft against the team's
|
|
324
|
+
* current org+platform context. Returns the composed canonical output
|
|
325
|
+
* plus per-field conflicts where the draft was tightened by the
|
|
326
|
+
* org/platform floor. No DB writes.
|
|
327
|
+
*/
|
|
328
|
+
export async function previewComposeTeamTemplate(teamId, kind, yamlBody) {
|
|
329
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template/preview-compose`);
|
|
330
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
...(await authHeaders()),
|
|
334
|
+
"Content-Type": "text/yaml",
|
|
335
|
+
Accept: "application/json",
|
|
336
|
+
},
|
|
337
|
+
body: yamlBody,
|
|
338
|
+
}));
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
341
|
+
throw new Error(err.message || `Preview failed: ${response.status}`);
|
|
342
|
+
}
|
|
343
|
+
const body = (await response.json());
|
|
344
|
+
return { composed: body.composed, conflicts: body.conflicts };
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* POST /v1/teams/:team_id/admins — grant team_admin role on a team.
|
|
348
|
+
*
|
|
349
|
+
* Per ADR-046 §3: callable by org_owner / org_admin only. The target
|
|
350
|
+
* user must already be a member of the team's org; cross-org grants 403.
|
|
351
|
+
* Idempotent — re-granting an already-active grant returns 200 with
|
|
352
|
+
* `idempotent_noop: true`.
|
|
353
|
+
*/
|
|
354
|
+
export async function grantTeamAdmin(teamId, userId) {
|
|
355
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/admins`);
|
|
356
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: {
|
|
359
|
+
...(await authHeaders()),
|
|
360
|
+
"Content-Type": "application/json",
|
|
361
|
+
Accept: "application/json",
|
|
362
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify({ user_id: sanitizeForHttp(userId) }),
|
|
365
|
+
}));
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
if (response.status === 401) {
|
|
368
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
369
|
+
}
|
|
370
|
+
if (response.status === 403) {
|
|
371
|
+
throw new Error(`Forbidden: org owner or admin role required, and target user must be in the team's org.`);
|
|
372
|
+
}
|
|
373
|
+
if (response.status === 404) {
|
|
374
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
375
|
+
}
|
|
376
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
377
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
378
|
+
}
|
|
379
|
+
return (await response.json());
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* DELETE /v1/teams/:team_id/admins/:user_id — revoke a team_admin grant.
|
|
383
|
+
*
|
|
384
|
+
* Per ADR-046 §3: callable by org_owner / org_admin OR self-revoke (the
|
|
385
|
+
* team_admin revokes their own grant — the only self-mutation). Idempotent
|
|
386
|
+
* — revoking an absent or already-revoked grant returns 200 with
|
|
387
|
+
* `idempotent_noop: true` and no audit row.
|
|
388
|
+
*/
|
|
389
|
+
export async function revokeTeamAdmin(teamId, userId) {
|
|
390
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/admins/${userId}`);
|
|
391
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
392
|
+
method: "DELETE",
|
|
393
|
+
headers: {
|
|
394
|
+
...(await authHeaders()),
|
|
395
|
+
Accept: "application/json",
|
|
396
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
397
|
+
},
|
|
398
|
+
}));
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
if (response.status === 401) {
|
|
401
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
402
|
+
}
|
|
403
|
+
if (response.status === 403) {
|
|
404
|
+
throw new Error(`Forbidden: revoke requires org owner/admin, or self-revoke with an active grant.`);
|
|
405
|
+
}
|
|
406
|
+
if (response.status === 404) {
|
|
407
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
408
|
+
}
|
|
409
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
410
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
411
|
+
}
|
|
412
|
+
return (await response.json());
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* GET /v1/teams/:team_id/admins — list active team_admin grants on a team.
|
|
416
|
+
* Any member of the team's org can read.
|
|
417
|
+
*/
|
|
418
|
+
export async function listTeamAdmins(teamId) {
|
|
419
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/admins`);
|
|
420
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
421
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
422
|
+
}));
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
if (response.status === 401) {
|
|
425
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
426
|
+
}
|
|
427
|
+
if (response.status === 403) {
|
|
428
|
+
throw new Error(`Forbidden: requires membership in the team's org.`);
|
|
429
|
+
}
|
|
430
|
+
if (response.status === 404) {
|
|
431
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
432
|
+
}
|
|
433
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
434
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
435
|
+
}
|
|
436
|
+
return (await response.json());
|
|
437
|
+
}
|
|
136
438
|
/**
|
|
137
439
|
* Look up an agent in the authenticated user's account by name.
|
|
138
440
|
* Tries exact match first, then single partial match.
|
|
@@ -143,14 +445,14 @@ export async function listAgents() {
|
|
|
143
445
|
export async function getAgentByName(name) {
|
|
144
446
|
const agents = await listAgents();
|
|
145
447
|
const lower = name.toLowerCase();
|
|
146
|
-
const exact = agents.find(a => a.name?.toLowerCase() === lower);
|
|
448
|
+
const exact = agents.find((a) => a.name?.toLowerCase() === lower);
|
|
147
449
|
if (exact)
|
|
148
450
|
return exact;
|
|
149
|
-
const partials = agents.filter(a => a.name?.toLowerCase().includes(lower));
|
|
451
|
+
const partials = agents.filter((a) => a.name?.toLowerCase().includes(lower));
|
|
150
452
|
if (partials.length === 1)
|
|
151
453
|
return partials[0];
|
|
152
454
|
if (partials.length > 1) {
|
|
153
|
-
const names = partials.map(a => a.name ?? a.id).join(", ");
|
|
455
|
+
const names = partials.map((a) => a.name ?? a.id).join(", ");
|
|
154
456
|
throw new Error(`Multiple agents match '${name}': ${names}. Use a more specific name.`);
|
|
155
457
|
}
|
|
156
458
|
return null;
|
|
@@ -491,3 +793,372 @@ export async function resolveAgentId(agentName) {
|
|
|
491
793
|
console.error("List your agents with: mnemom agents\n");
|
|
492
794
|
process.exit(1);
|
|
493
795
|
}
|
|
796
|
+
/** GET /v1/postures — list visible postures. */
|
|
797
|
+
export async function listPostures(opts) {
|
|
798
|
+
const params = new URLSearchParams();
|
|
799
|
+
if (opts.orgId)
|
|
800
|
+
params.set("org_id", opts.orgId);
|
|
801
|
+
if (opts.includePlatform === false)
|
|
802
|
+
params.set("include_platform", "false");
|
|
803
|
+
if (opts.includeDeleted)
|
|
804
|
+
params.set("include_deleted", "true");
|
|
805
|
+
const qs = params.toString();
|
|
806
|
+
const url = validateUrl(`${API_BASE}/v1/postures${qs ? `?${qs}` : ""}`);
|
|
807
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
808
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
809
|
+
}));
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
if (response.status === 401) {
|
|
812
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
813
|
+
}
|
|
814
|
+
if (response.status === 403) {
|
|
815
|
+
throw new Error(`Forbidden: not a member of org '${opts.orgId}'.`);
|
|
816
|
+
}
|
|
817
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
818
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
819
|
+
}
|
|
820
|
+
const data = (await response.json());
|
|
821
|
+
return data.postures ?? [];
|
|
822
|
+
}
|
|
823
|
+
/** GET /v1/postures/:id — read posture with current revision body. */
|
|
824
|
+
export async function getPosture(postureId) {
|
|
825
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}`);
|
|
826
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
827
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
828
|
+
}));
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
if (response.status === 401) {
|
|
831
|
+
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
832
|
+
}
|
|
833
|
+
if (response.status === 404)
|
|
834
|
+
throw new Error(`Posture '${postureId}' not found.`);
|
|
835
|
+
if (response.status === 403)
|
|
836
|
+
throw new Error(`Forbidden: not visible to your account.`);
|
|
837
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
838
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
839
|
+
}
|
|
840
|
+
return (await response.json());
|
|
841
|
+
}
|
|
842
|
+
/** GET /v1/postures/:id/revisions — list revisions, newest first. */
|
|
843
|
+
export async function listPostureRevisions(postureId) {
|
|
844
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/revisions`);
|
|
845
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
846
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
847
|
+
}));
|
|
848
|
+
if (!response.ok) {
|
|
849
|
+
if (response.status === 401)
|
|
850
|
+
throw new Error("Not authenticated.");
|
|
851
|
+
if (response.status === 404)
|
|
852
|
+
throw new Error(`Posture '${postureId}' not found.`);
|
|
853
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
854
|
+
throw new Error(err.message || `API request failed: ${response.status}`);
|
|
855
|
+
}
|
|
856
|
+
const data = (await response.json());
|
|
857
|
+
return data.revisions ?? [];
|
|
858
|
+
}
|
|
859
|
+
/** GET /v1/postures/:id/diff?from=N&to=M — structural diff. */
|
|
860
|
+
export async function diffPostureRevisions(postureId, fromNo, toNo) {
|
|
861
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/diff?from=${fromNo}&to=${toNo}`);
|
|
862
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
863
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
864
|
+
}));
|
|
865
|
+
if (!response.ok) {
|
|
866
|
+
if (response.status === 401)
|
|
867
|
+
throw new Error("Not authenticated.");
|
|
868
|
+
if (response.status === 404) {
|
|
869
|
+
const errBody = (await response.json().catch(() => ({})));
|
|
870
|
+
throw new Error(errBody.message || `Posture or revision not found.`);
|
|
871
|
+
}
|
|
872
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
873
|
+
throw new Error(err.message || `Diff failed: ${response.status}`);
|
|
874
|
+
}
|
|
875
|
+
return (await response.json());
|
|
876
|
+
}
|
|
877
|
+
/** POST /v1/postures — create new posture (org-scope only via REST). */
|
|
878
|
+
export async function createPosture(input) {
|
|
879
|
+
const url = validateUrl(`${API_BASE}/v1/postures`);
|
|
880
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
881
|
+
method: "POST",
|
|
882
|
+
headers: {
|
|
883
|
+
...(await authHeaders()),
|
|
884
|
+
"Content-Type": "application/json",
|
|
885
|
+
Accept: "application/json",
|
|
886
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
887
|
+
},
|
|
888
|
+
body: JSON.stringify(input),
|
|
889
|
+
}));
|
|
890
|
+
if (!response.ok) {
|
|
891
|
+
if (response.status === 401)
|
|
892
|
+
throw new Error("Not authenticated.");
|
|
893
|
+
if (response.status === 403) {
|
|
894
|
+
throw new Error(`Forbidden: org owner/admin role required (or scope=platform reserved for migrations).`);
|
|
895
|
+
}
|
|
896
|
+
if (response.status === 409)
|
|
897
|
+
throw new Error(`A posture with this slug already exists in the org.`);
|
|
898
|
+
if (response.status === 413)
|
|
899
|
+
throw new Error(`Posture body too large (server limit: 256 KiB).`);
|
|
900
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
901
|
+
throw new Error(err.message || `Create failed: ${response.status}`);
|
|
902
|
+
}
|
|
903
|
+
return (await response.json());
|
|
904
|
+
}
|
|
905
|
+
/** PUT /v1/postures/:id — write a new revision (forward-only). */
|
|
906
|
+
export async function updatePosture(postureId, input) {
|
|
907
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}`);
|
|
908
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
909
|
+
method: "PUT",
|
|
910
|
+
headers: {
|
|
911
|
+
...(await authHeaders()),
|
|
912
|
+
"Content-Type": "application/json",
|
|
913
|
+
Accept: "application/json",
|
|
914
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
915
|
+
},
|
|
916
|
+
body: JSON.stringify(input),
|
|
917
|
+
}));
|
|
918
|
+
if (!response.ok) {
|
|
919
|
+
if (response.status === 401)
|
|
920
|
+
throw new Error("Not authenticated.");
|
|
921
|
+
if (response.status === 403) {
|
|
922
|
+
throw new Error(`Forbidden: org owner/admin required, or this is a Mnemom-shipped platform default (immutable — clone first).`);
|
|
923
|
+
}
|
|
924
|
+
if (response.status === 404)
|
|
925
|
+
throw new Error(`Posture '${postureId}' not found.`);
|
|
926
|
+
if (response.status === 413)
|
|
927
|
+
throw new Error(`Posture body too large (server limit: 256 KiB).`);
|
|
928
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
929
|
+
throw new Error(err.message || `Update failed: ${response.status}`);
|
|
930
|
+
}
|
|
931
|
+
return (await response.json());
|
|
932
|
+
}
|
|
933
|
+
/** POST /v1/postures/:id/clone — clone source to org-scope. */
|
|
934
|
+
export async function clonePosture(postureId, input) {
|
|
935
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/clone`);
|
|
936
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: {
|
|
939
|
+
...(await authHeaders()),
|
|
940
|
+
"Content-Type": "application/json",
|
|
941
|
+
Accept: "application/json",
|
|
942
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
943
|
+
},
|
|
944
|
+
body: JSON.stringify(input),
|
|
945
|
+
}));
|
|
946
|
+
if (!response.ok) {
|
|
947
|
+
if (response.status === 401)
|
|
948
|
+
throw new Error("Not authenticated.");
|
|
949
|
+
if (response.status === 403)
|
|
950
|
+
throw new Error(`Forbidden: org owner/admin required on target org.`);
|
|
951
|
+
if (response.status === 404)
|
|
952
|
+
throw new Error(`Source posture '${postureId}' not found.`);
|
|
953
|
+
if (response.status === 409)
|
|
954
|
+
throw new Error(`A posture with this slug already exists in the target org.`);
|
|
955
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
956
|
+
throw new Error(err.message || `Clone failed: ${response.status}`);
|
|
957
|
+
}
|
|
958
|
+
return (await response.json());
|
|
959
|
+
}
|
|
960
|
+
/** DELETE /v1/postures/:id — soft-delete (refuses if assigned). */
|
|
961
|
+
export async function deletePosture(postureId) {
|
|
962
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}`);
|
|
963
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
964
|
+
method: "DELETE",
|
|
965
|
+
headers: {
|
|
966
|
+
...(await authHeaders()),
|
|
967
|
+
Accept: "application/json",
|
|
968
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
969
|
+
},
|
|
970
|
+
}));
|
|
971
|
+
if (!response.ok) {
|
|
972
|
+
if (response.status === 401)
|
|
973
|
+
throw new Error("Not authenticated.");
|
|
974
|
+
if (response.status === 403)
|
|
975
|
+
throw new Error(`Forbidden: platform defaults are immutable.`);
|
|
976
|
+
if (response.status === 404)
|
|
977
|
+
throw new Error(`Posture '${postureId}' not found.`);
|
|
978
|
+
if (response.status === 409) {
|
|
979
|
+
throw new Error(`Cannot delete a posture that is currently assigned to one or more teams. Unassign first.`);
|
|
980
|
+
}
|
|
981
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
982
|
+
throw new Error(err.message || `Delete failed: ${response.status}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/** POST /v1/postures/:id/assign — assign to a team. */
|
|
986
|
+
export async function assignPosture(postureId, teamId, pinRevisionNo) {
|
|
987
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/assign`);
|
|
988
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
989
|
+
method: "POST",
|
|
990
|
+
headers: {
|
|
991
|
+
...(await authHeaders()),
|
|
992
|
+
"Content-Type": "application/json",
|
|
993
|
+
Accept: "application/json",
|
|
994
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
995
|
+
},
|
|
996
|
+
body: JSON.stringify({ team_id: teamId, pin_revision_no: pinRevisionNo ?? null }),
|
|
997
|
+
}));
|
|
998
|
+
if (!response.ok) {
|
|
999
|
+
if (response.status === 401)
|
|
1000
|
+
throw new Error("Not authenticated.");
|
|
1001
|
+
if (response.status === 403)
|
|
1002
|
+
throw new Error(`Forbidden: org owner/admin required on team's org.`);
|
|
1003
|
+
if (response.status === 404) {
|
|
1004
|
+
const errBody = (await response.json().catch(() => ({})));
|
|
1005
|
+
throw new Error(errBody.message || `Posture, team, or pinned revision not found.`);
|
|
1006
|
+
}
|
|
1007
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1008
|
+
throw new Error(err.message || `Assign failed: ${response.status}`);
|
|
1009
|
+
}
|
|
1010
|
+
return (await response.json());
|
|
1011
|
+
}
|
|
1012
|
+
/** DELETE /v1/postures/:id/assignments/:team_id — remove an assignment. */
|
|
1013
|
+
export async function unassignPosture(postureId, teamId) {
|
|
1014
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/assignments/${teamId}`);
|
|
1015
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1016
|
+
method: "DELETE",
|
|
1017
|
+
headers: {
|
|
1018
|
+
...(await authHeaders()),
|
|
1019
|
+
Accept: "application/json",
|
|
1020
|
+
"Idempotency-Key": newIdempotencyKey(),
|
|
1021
|
+
},
|
|
1022
|
+
}));
|
|
1023
|
+
if (!response.ok) {
|
|
1024
|
+
if (response.status === 401)
|
|
1025
|
+
throw new Error("Not authenticated.");
|
|
1026
|
+
if (response.status === 403)
|
|
1027
|
+
throw new Error(`Forbidden: org owner/admin required.`);
|
|
1028
|
+
if (response.status === 404) {
|
|
1029
|
+
throw new Error(`No matching assignment to remove (posture '${postureId}' is not assigned to team '${teamId}').`);
|
|
1030
|
+
}
|
|
1031
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1032
|
+
throw new Error(err.message || `Unassign failed: ${response.status}`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/** POST /v1/postures/:id/preview-compose — preview effective for a team. */
|
|
1036
|
+
export async function previewComposePosture(postureId, teamId) {
|
|
1037
|
+
const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/preview-compose`);
|
|
1038
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
headers: {
|
|
1041
|
+
...(await authHeaders()),
|
|
1042
|
+
"Content-Type": "application/json",
|
|
1043
|
+
Accept: "application/json",
|
|
1044
|
+
},
|
|
1045
|
+
body: JSON.stringify({ team_id: teamId }),
|
|
1046
|
+
}));
|
|
1047
|
+
if (!response.ok) {
|
|
1048
|
+
if (response.status === 401)
|
|
1049
|
+
throw new Error("Not authenticated.");
|
|
1050
|
+
if (response.status === 404) {
|
|
1051
|
+
const errBody = (await response.json().catch(() => ({})));
|
|
1052
|
+
throw new Error(errBody.message || `Posture or team not found.`);
|
|
1053
|
+
}
|
|
1054
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1055
|
+
throw new Error(err.message || `Preview-compose failed: ${response.status}`);
|
|
1056
|
+
}
|
|
1057
|
+
return (await response.json());
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* GET /v1/agents/:id/sideband-advisories
|
|
1061
|
+
*
|
|
1062
|
+
* Lists pending_advisories rows where source LIKE 'sideband.%' for the
|
|
1063
|
+
* given agent. Default page 50, max 200. Optional ?since=<ISO> filter.
|
|
1064
|
+
*/
|
|
1065
|
+
export async function listSidebandAdvisoriesForAgent(agentId, opts = {}) {
|
|
1066
|
+
const params = new URLSearchParams();
|
|
1067
|
+
if (opts.limit)
|
|
1068
|
+
params.set("limit", String(opts.limit));
|
|
1069
|
+
if (opts.since)
|
|
1070
|
+
params.set("since", opts.since);
|
|
1071
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1072
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/sideband-advisories${qs}`);
|
|
1073
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1074
|
+
method: "GET",
|
|
1075
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
1076
|
+
}));
|
|
1077
|
+
if (!response.ok) {
|
|
1078
|
+
if (response.status === 401)
|
|
1079
|
+
throw new Error("Not authenticated.");
|
|
1080
|
+
if (response.status === 403)
|
|
1081
|
+
throw new Error("Permission denied (need org membership).");
|
|
1082
|
+
if (response.status === 404)
|
|
1083
|
+
throw new Error(`Agent '${agentId}' not found.`);
|
|
1084
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1085
|
+
throw new Error(err.message || `List advisories failed: ${response.status}`);
|
|
1086
|
+
}
|
|
1087
|
+
return (await response.json());
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* GET /v1/teams/:id/sideband-advisories
|
|
1091
|
+
*
|
|
1092
|
+
* Lists pending_advisories rows for any agent that is a current member of
|
|
1093
|
+
* the team. Default page 100, max 500. Optional ?since=<ISO> filter.
|
|
1094
|
+
*/
|
|
1095
|
+
export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
|
|
1096
|
+
const params = new URLSearchParams();
|
|
1097
|
+
if (opts.limit)
|
|
1098
|
+
params.set("limit", String(opts.limit));
|
|
1099
|
+
if (opts.since)
|
|
1100
|
+
params.set("since", opts.since);
|
|
1101
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1102
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-advisories${qs}`);
|
|
1103
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1104
|
+
method: "GET",
|
|
1105
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
1106
|
+
}));
|
|
1107
|
+
if (!response.ok) {
|
|
1108
|
+
if (response.status === 401)
|
|
1109
|
+
throw new Error("Not authenticated.");
|
|
1110
|
+
if (response.status === 403)
|
|
1111
|
+
throw new Error("Permission denied (need org membership).");
|
|
1112
|
+
if (response.status === 404)
|
|
1113
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
1114
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1115
|
+
throw new Error(err.message || `List advisories failed: ${response.status}`);
|
|
1116
|
+
}
|
|
1117
|
+
return (await response.json());
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* GET /v1/teams/:id/sideband-coverage
|
|
1121
|
+
*
|
|
1122
|
+
* Compliance-grade proof-of-coverage for a team — last 30 days of
|
|
1123
|
+
* per-axis sweep aggregates + heartbeat. Returns per-axis summary +
|
|
1124
|
+
* raw rows.
|
|
1125
|
+
*/
|
|
1126
|
+
export async function getTeamSidebandCoverage(teamId) {
|
|
1127
|
+
const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-coverage`);
|
|
1128
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1129
|
+
method: "GET",
|
|
1130
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
1131
|
+
}));
|
|
1132
|
+
if (!response.ok) {
|
|
1133
|
+
if (response.status === 401)
|
|
1134
|
+
throw new Error("Not authenticated.");
|
|
1135
|
+
if (response.status === 403)
|
|
1136
|
+
throw new Error("Permission denied (need org membership).");
|
|
1137
|
+
if (response.status === 404)
|
|
1138
|
+
throw new Error(`Team '${teamId}' not found.`);
|
|
1139
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1140
|
+
throw new Error(err.message || `Coverage fetch failed: ${response.status}`);
|
|
1141
|
+
}
|
|
1142
|
+
return (await response.json());
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* GET /v1/admin/safe-house/harness-state — fetch the most recent
|
|
1146
|
+
* harness run per lane (full + fast). Powers `mnemom validate
|
|
1147
|
+
* safe-house`. Admin-gated; returns 403 for non-mnemom-staff.
|
|
1148
|
+
*/
|
|
1149
|
+
export async function getSafeHouseHarnessState() {
|
|
1150
|
+
const url = validateUrl(`${API_BASE}/v1/admin/safe-house/harness-state`);
|
|
1151
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
1152
|
+
method: "GET",
|
|
1153
|
+
headers: { ...(await authHeaders()), Accept: "application/json" },
|
|
1154
|
+
}));
|
|
1155
|
+
if (!response.ok) {
|
|
1156
|
+
if (response.status === 401)
|
|
1157
|
+
throw new Error("Not authenticated. Run `mnemom login`.");
|
|
1158
|
+
if (response.status === 403)
|
|
1159
|
+
throw new Error("Permission denied — `mnemom validate safe-house` is mnemom-staff only.");
|
|
1160
|
+
const err = (await response.json().catch(() => ({ error: "unknown" })));
|
|
1161
|
+
throw new Error(err.message || `Harness state fetch failed: ${response.status}`);
|
|
1162
|
+
}
|
|
1163
|
+
return (await response.json());
|
|
1164
|
+
}
|