@mnemom/mnemom 0.16.2 → 0.16.3

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/lib/api.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getApiUrl } from "./config.js";
3
3
  import { forceRefreshAccessToken, resolveAuth } from "./auth.js";
4
+ import { mnemomFetch } from "./version-gate.js";
4
5
  export const API_BASE = getApiUrl();
6
+ const NOT_AUTHENTICATED_MSG = "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.";
5
7
  /** Sanitize file-sourced data before use in outbound HTTP requests. */
6
8
  function sanitizeForHttp(data) {
7
9
  return String(data).trim();
@@ -112,7 +114,7 @@ export async function readApiError(response, fallback) {
112
114
  }
113
115
  async function fetchApi(endpoint) {
114
116
  const url = validateUrl(`${API_BASE}${endpoint}`);
115
- const response = await fetch(url);
117
+ const response = await mnemomFetch(url);
116
118
  if (!response.ok) {
117
119
  throw await readApiError(response, "API request failed");
118
120
  }
@@ -120,18 +122,12 @@ async function fetchApi(endpoint) {
120
122
  }
121
123
  export async function postApi(endpoint, body, opts = {}) {
122
124
  const url = validateUrl(`${API_BASE}${endpoint}`);
123
- const cred = await resolveAuth();
124
125
  const headers = {
125
126
  "Content-Type": "application/json",
126
127
  "Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
128
+ ...(await authHeaders()),
127
129
  };
128
- if (cred.type === "jwt") {
129
- headers["Authorization"] = `Bearer ${cred.token}`;
130
- }
131
- else if (cred.type === "api-key") {
132
- headers["X-Mnemom-Api-Key"] = cred.key;
133
- }
134
- const response = await fetch(url, {
130
+ const response = await mnemomFetch(url, {
135
131
  // lgtm[js/file-data-url]
136
132
  method: "POST",
137
133
  headers,
@@ -193,13 +189,13 @@ async function authHeaders() {
193
189
  * server-side reservation by design.
194
190
  */
195
191
  async function fetchWithAuthRetry(url, buildInit) {
196
- const first = await fetch(url, await buildInit());
192
+ const first = await mnemomFetch(url, await buildInit());
197
193
  if (first.status !== 401)
198
194
  return first;
199
195
  const refreshed = await forceRefreshAccessToken();
200
196
  if (!refreshed)
201
197
  return first;
202
- return fetch(url, await buildInit());
198
+ return mnemomFetch(url, await buildInit());
203
199
  }
204
200
  export async function getAgent(id) {
205
201
  return fetchApi(`/v1/agents/${id}`);
@@ -212,17 +208,7 @@ export async function getAgent(id) {
212
208
  * routing here avoids the false "Not authenticated" error (MNE-194 F10).
213
209
  */
214
210
  export async function listAgents() {
215
- const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
216
- const response = await fetchWithAuthRetry(url, async () => ({
217
- headers: await authHeaders(),
218
- }));
219
- if (!response.ok) {
220
- if (response.status === 401) {
221
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
222
- }
223
- throw await readApiError(response, "Failed to list agents");
224
- }
225
- const data = (await response.json());
211
+ const data = await gFetch("/v1/agents?limit=100", {}, { fallback: "Failed to list agents" });
226
212
  return data.agents ?? [];
227
213
  }
228
214
  /**
@@ -231,17 +217,7 @@ export async function listAgents() {
231
217
  * first; multi-user orgs follow.
232
218
  */
233
219
  export async function listMyOrgs() {
234
- const url = validateUrl(`${API_BASE}/v1/orgs`);
235
- const response = await fetchWithAuthRetry(url, async () => ({
236
- headers: await authHeaders(),
237
- }));
238
- if (!response.ok) {
239
- if (response.status === 401) {
240
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
241
- }
242
- throw await readApiError(response, "API request failed");
243
- }
244
- const data = (await response.json());
220
+ const data = await gFetch("/v1/orgs", {}, { fallback: "API request failed" });
245
221
  return data.orgs ?? [];
246
222
  }
247
223
  /**
@@ -249,17 +225,7 @@ export async function listMyOrgs() {
249
225
  * member of the org may read it; non-members get 403.
250
226
  */
251
227
  export async function fetchOrgFleet(orgId) {
252
- const url = validateUrl(`${API_BASE}/v1/orgs/${encodeURIComponent(orgId)}/agents`);
253
- const response = await fetchWithAuthRetry(url, async () => ({
254
- headers: { ...(await authHeaders()), Accept: "application/json" },
255
- }));
256
- if (!response.ok) {
257
- if (response.status === 401) {
258
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
259
- }
260
- throw await readApiError(response, "Failed to list org agents");
261
- }
262
- const data = (await response.json());
228
+ const data = await gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/agents`, {}, { fallback: "Failed to list org agents" });
263
229
  return data.agents ?? [];
264
230
  }
265
231
  function fleetToRow(a, org) {
@@ -351,7 +317,7 @@ export async function claimAgent(agentId, body, opts = {}) {
351
317
  }));
352
318
  if (!response.ok) {
353
319
  if (response.status === 401) {
354
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
320
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
355
321
  }
356
322
  throw await readApiError(response, "Failed to claim agent");
357
323
  }
@@ -366,22 +332,48 @@ export async function claimAgent(agentId, body, opts = {}) {
366
332
  };
367
333
  }
368
334
  /**
369
- * GET /v1/auth/me/personal-orgaccessor for the user's personal org.
370
- * Idempotent: lazily provisions for legacy accounts that pre-date the
371
- * mnemom-api migration 161 backfill.
335
+ * POST /v1/agents/:id/moverole-based relocation between orgs.
336
+ *
337
+ * The role-based counterpart to claimAgent's re-claim: the server requires
338
+ * the caller to be an owner/admin of BOTH the agent's current org and
339
+ * `destOrgId` — no hash proof / agent key involved. Moving to the current
340
+ * org is an idempotent no-op (`moved: false`). An Idempotency-Key is minted
341
+ * (held across the 401-refresh retry) like every mutation.
372
342
  */
373
- export async function getMyPersonalOrg() {
374
- const url = validateUrl(`${API_BASE}/v1/auth/me/personal-org`);
343
+ export async function moveAgent(agentId, destOrgId, opts = {}) {
344
+ const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/move`);
345
+ const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
375
346
  const response = await fetchWithAuthRetry(url, async () => ({
376
- headers: await authHeaders(),
347
+ method: "POST",
348
+ headers: {
349
+ ...(await authHeaders()),
350
+ "Content-Type": "application/json",
351
+ Accept: "application/json",
352
+ "Idempotency-Key": idempotencyKey,
353
+ },
354
+ body: JSON.stringify({ dest_org_id: sanitizeForHttp(destOrgId) }),
377
355
  }));
378
356
  if (!response.ok) {
379
357
  if (response.status === 401) {
380
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
358
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
381
359
  }
382
- throw await readApiError(response, "API request failed");
360
+ throw await readApiError(response, "Failed to move agent");
383
361
  }
384
- return (await response.json());
362
+ const data = (await response.json());
363
+ return {
364
+ moved: data.moved ?? true,
365
+ agent_id: data.agent_id ?? agentId,
366
+ from_org_id: data.from_org_id ?? null,
367
+ to_org_id: data.to_org_id ?? null,
368
+ };
369
+ }
370
+ /**
371
+ * GET /v1/auth/me/personal-org — accessor for the user's personal org.
372
+ * Idempotent: lazily provisions for legacy accounts that pre-date the
373
+ * mnemom-api migration 161 backfill.
374
+ */
375
+ export async function getMyPersonalOrg() {
376
+ return gFetch("/v1/auth/me/personal-org", {}, { fallback: "API request failed" });
385
377
  }
386
378
  /**
387
379
  * GET /v1/orgs/:org_id/teams for every org the user is a member of,
@@ -421,7 +413,7 @@ export async function getTeam(teamId) {
421
413
  }));
422
414
  if (!response.ok) {
423
415
  if (response.status === 401) {
424
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
416
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
425
417
  }
426
418
  if (response.status === 404) {
427
419
  throw new MnemomApiError(404, `Team '${teamId}' not found or not accessible.`);
@@ -443,7 +435,7 @@ export async function getTeamTemplate(teamId, kind) {
443
435
  }));
444
436
  if (!response.ok) {
445
437
  if (response.status === 401) {
446
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
438
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
447
439
  }
448
440
  if (response.status === 403) {
449
441
  throw new MnemomApiError(403, `Forbidden: not a member of team '${teamId}'s org.`);
@@ -479,7 +471,7 @@ export async function putTeamTemplate(teamId, kind, yamlBody) {
479
471
  }));
480
472
  if (!response.ok) {
481
473
  if (response.status === 401) {
482
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
474
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
483
475
  }
484
476
  if (response.status === 403) {
485
477
  throw new MnemomApiError(403, `Forbidden: org admin or owner role required to write a team template.`);
@@ -512,7 +504,7 @@ export async function deleteTeamTemplate(teamId, kind) {
512
504
  }));
513
505
  if (!response.ok) {
514
506
  if (response.status === 401) {
515
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
507
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
516
508
  }
517
509
  if (response.status === 403) {
518
510
  throw new MnemomApiError(403, `Forbidden: org admin or owner role required to clear a team template.`);
@@ -570,7 +562,7 @@ export async function grantTeamAdmin(teamId, userId) {
570
562
  }));
571
563
  if (!response.ok) {
572
564
  if (response.status === 401) {
573
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
565
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
574
566
  }
575
567
  if (response.status === 403) {
576
568
  throw new MnemomApiError(403, `Forbidden: org owner or admin role required, and target user must be in the team's org.`);
@@ -602,7 +594,7 @@ export async function revokeTeamAdmin(teamId, userId) {
602
594
  }));
603
595
  if (!response.ok) {
604
596
  if (response.status === 401) {
605
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
597
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
606
598
  }
607
599
  if (response.status === 403) {
608
600
  throw new MnemomApiError(403, `Forbidden: revoke requires org owner/admin, or self-revoke with an active grant.`);
@@ -625,7 +617,7 @@ export async function listTeamAdmins(teamId) {
625
617
  }));
626
618
  if (!response.ok) {
627
619
  if (response.status === 401) {
628
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
620
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
629
621
  }
630
622
  if (response.status === 403) {
631
623
  throw new MnemomApiError(403, `Forbidden: requires membership in the team's org.`);
@@ -672,17 +664,10 @@ export async function getAgentByName(name) {
672
664
  * path from PR #200 so a stale local expiresAt heals transparently.
673
665
  */
674
666
  export async function getIntegrity(id) {
675
- const url = validateUrl(`${API_BASE}/v1/integrity/${id}`);
676
- const response = await fetchWithAuthRetry(url, async () => ({
677
- headers: await authHeaders(),
678
- }));
679
- if (!response.ok) {
680
- throw await readApiError(response, "API request failed");
681
- }
667
+ const body = await gFetch(`/v1/integrity/${id}`, {}, { fallback: "API request failed" });
682
668
  // Accept both the canonical docs shape (object) and the RPC-wrapped shape
683
669
  // (array of one row). The latter is what prod actually returns today on
684
670
  // the RPC path; flagged for a follow-up API normalization.
685
- const body = (await response.json());
686
671
  const row = Array.isArray(body) ? (body[0] ?? emptyIntegrityRow(id)) : body;
687
672
  // Fill in agent_id when the API didn't (RPC path) so the field is always
688
673
  // populated for consumers regardless of which API path served the request.
@@ -714,14 +699,7 @@ function emptyIntegrityRow(agentId) {
714
699
  * don't need a special case for it.
715
700
  */
716
701
  export async function getTraces(id, limit = 10) {
717
- const url = validateUrl(`${API_BASE}/v1/traces?agent_id=${id}&limit=${limit}`);
718
- const response = await fetchWithAuthRetry(url, async () => ({
719
- headers: await authHeaders(),
720
- }));
721
- if (!response.ok) {
722
- throw await readApiError(response, "API request failed");
723
- }
724
- const data = (await response.json());
702
+ const data = await gFetch(`/v1/traces?agent_id=${id}&limit=${limit}`, {}, { fallback: "API request failed" });
725
703
  // Accept both the envelope shape (current API) and a bare array (defensive
726
704
  // — staging is mid-deploy when a CLI smoke runs against an older API).
727
705
  if (Array.isArray(data))
@@ -741,7 +719,7 @@ export async function getAlignmentCard(agentId, format = "yaml") {
741
719
  // W1.2b). The legacy `/v1/agents/:id/alignment-card` only 308-redirects to
742
720
  // this — first-party callers target canonical directly (no legacy shapes).
743
721
  const url = validateUrl(`${API_BASE}/v1/alignment/agent/${encodeURIComponent(agentId)}`);
744
- const response = await fetch(url, {
722
+ const response = await mnemomFetch(url, {
745
723
  headers: { Accept: accept, ...(await authHeaders()) },
746
724
  });
747
725
  if (!response.ok) {
@@ -801,7 +779,7 @@ export async function getProtectionCard(agentId, format = "yaml") {
801
779
  // Canonical surface (ADR-062 / W1.2b); legacy `/v1/agents/:id/protection-card`
802
780
  // only 308-redirects here. First-party callers use canonical directly.
803
781
  const url = validateUrl(`${API_BASE}/v1/protection/agent/${encodeURIComponent(agentId)}`);
804
- const response = await fetch(url, {
782
+ const response = await mnemomFetch(url, {
805
783
  headers: { Accept: accept, ...(await authHeaders()) },
806
784
  });
807
785
  if (!response.ok) {
@@ -869,7 +847,7 @@ export async function previewComposeAgentCard(agentId, kind, body, contentType =
869
847
  }
870
848
  // 401 → unauthenticated: signal the caller to fall back to offline validation.
871
849
  if (response.status === 401) {
872
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
850
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
873
851
  }
874
852
  // 400 / 422 → the card is invalid: an expected validation outcome, not an
875
853
  // exception. Surface the care-framed findings structurally.
@@ -917,7 +895,7 @@ export async function resolveAgentId(agentName) {
917
895
  catch (err) {
918
896
  const msg = err instanceof Error ? err.message : String(err);
919
897
  if (msg.includes("Not authenticated")) {
920
- console.error(`\nNot authenticated. Run \`mnemom login\` or set MNEMOM_API_KEY.\n`);
898
+ console.error(`\n${NOT_AUTHENTICATED_MSG}\n`);
921
899
  process.exit(1);
922
900
  }
923
901
  }
@@ -945,7 +923,7 @@ export async function listPostures(opts) {
945
923
  }));
946
924
  if (!response.ok) {
947
925
  if (response.status === 401) {
948
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
926
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
949
927
  }
950
928
  if (response.status === 403) {
951
929
  throw new MnemomApiError(403, `Forbidden: not a member of org '${opts.orgId}'.`);
@@ -963,7 +941,7 @@ export async function getPosture(postureId) {
963
941
  }));
964
942
  if (!response.ok) {
965
943
  if (response.status === 401) {
966
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
944
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
967
945
  }
968
946
  if (response.status === 404)
969
947
  throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
@@ -975,18 +953,7 @@ export async function getPosture(postureId) {
975
953
  }
976
954
  /** GET /v1/postures/:id/revisions — list revisions, newest first. */
977
955
  export async function listPostureRevisions(postureId) {
978
- const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/revisions`);
979
- const response = await fetchWithAuthRetry(url, async () => ({
980
- headers: { ...(await authHeaders()), Accept: "application/json" },
981
- }));
982
- if (!response.ok) {
983
- if (response.status === 401)
984
- throw new MnemomApiError(401, "Not authenticated.");
985
- if (response.status === 404)
986
- throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
987
- throw await readApiError(response, "API request failed");
988
- }
989
- const data = (await response.json());
956
+ const data = await gFetch(`/v1/postures/${postureId}/revisions`, {}, { notFoundLabel: `Posture '${postureId}'`, fallback: "API request failed" });
990
957
  return data.revisions ?? [];
991
958
  }
992
959
  /** GET /v1/postures/:id/diff?from=N&to=M — structural diff. */
@@ -997,7 +964,7 @@ export async function diffPostureRevisions(postureId, fromNo, toNo) {
997
964
  }));
998
965
  if (!response.ok) {
999
966
  if (response.status === 401)
1000
- throw new MnemomApiError(401, "Not authenticated.");
967
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1001
968
  if (response.status === 404) {
1002
969
  const errBody = await parseApiErrorBody(response);
1003
970
  throw new MnemomApiError(404, errBody.message || `Posture or revision not found.`, {
@@ -1025,7 +992,7 @@ export async function createPosture(input) {
1025
992
  }));
1026
993
  if (!response.ok) {
1027
994
  if (response.status === 401)
1028
- throw new MnemomApiError(401, "Not authenticated.");
995
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1029
996
  if (response.status === 403) {
1030
997
  throw new MnemomApiError(403, `Forbidden: org owner/admin role required (or scope=platform reserved for migrations).`);
1031
998
  }
@@ -1052,7 +1019,7 @@ export async function updatePosture(postureId, input) {
1052
1019
  }));
1053
1020
  if (!response.ok) {
1054
1021
  if (response.status === 401)
1055
- throw new MnemomApiError(401, "Not authenticated.");
1022
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1056
1023
  if (response.status === 403) {
1057
1024
  throw new MnemomApiError(403, `Forbidden: org owner/admin required, or this is a Mnemom-shipped platform default (immutable — clone first).`);
1058
1025
  }
@@ -1079,7 +1046,7 @@ export async function clonePosture(postureId, input) {
1079
1046
  }));
1080
1047
  if (!response.ok) {
1081
1048
  if (response.status === 401)
1082
- throw new MnemomApiError(401, "Not authenticated.");
1049
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1083
1050
  if (response.status === 403)
1084
1051
  throw new MnemomApiError(403, `Forbidden: org owner/admin required on target org.`);
1085
1052
  if (response.status === 404)
@@ -1103,7 +1070,7 @@ export async function deletePosture(postureId) {
1103
1070
  }));
1104
1071
  if (!response.ok) {
1105
1072
  if (response.status === 401)
1106
- throw new MnemomApiError(401, "Not authenticated.");
1073
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1107
1074
  if (response.status === 403)
1108
1075
  throw new MnemomApiError(403, `Forbidden: platform defaults are immutable.`);
1109
1076
  if (response.status === 404)
@@ -1129,7 +1096,7 @@ export async function assignPosture(postureId, teamId, pinRevisionNo) {
1129
1096
  }));
1130
1097
  if (!response.ok) {
1131
1098
  if (response.status === 401)
1132
- throw new MnemomApiError(401, "Not authenticated.");
1099
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1133
1100
  if (response.status === 403)
1134
1101
  throw new MnemomApiError(403, `Forbidden: org owner/admin required on team's org.`);
1135
1102
  if (response.status === 404) {
@@ -1153,7 +1120,7 @@ export async function unassignPosture(postureId, teamId) {
1153
1120
  }));
1154
1121
  if (!response.ok) {
1155
1122
  if (response.status === 401)
1156
- throw new MnemomApiError(401, "Not authenticated.");
1123
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1157
1124
  if (response.status === 403)
1158
1125
  throw new MnemomApiError(403, `Forbidden: org owner/admin required.`);
1159
1126
  if (response.status === 404) {
@@ -1176,7 +1143,7 @@ export async function previewComposePosture(postureId, teamId) {
1176
1143
  }));
1177
1144
  if (!response.ok) {
1178
1145
  if (response.status === 401)
1179
- throw new MnemomApiError(401, "Not authenticated.");
1146
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1180
1147
  if (response.status === 404) {
1181
1148
  const errBody = await parseApiErrorBody(response);
1182
1149
  throw new MnemomApiError(404, errBody.message || `Posture or team not found.`, {
@@ -1202,21 +1169,7 @@ export async function listSidebandAdvisoriesForAgent(agentId, opts = {}) {
1202
1169
  if (opts.since)
1203
1170
  params.set("since", opts.since);
1204
1171
  const qs = params.toString() ? `?${params.toString()}` : "";
1205
- const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/sideband-advisories${qs}`);
1206
- const response = await fetchWithAuthRetry(url, async () => ({
1207
- method: "GET",
1208
- headers: { ...(await authHeaders()), Accept: "application/json" },
1209
- }));
1210
- if (!response.ok) {
1211
- if (response.status === 401)
1212
- throw new MnemomApiError(401, "Not authenticated.");
1213
- if (response.status === 403)
1214
- throw new MnemomApiError(403, "Permission denied (need org membership).");
1215
- if (response.status === 404)
1216
- throw new MnemomApiError(404, `Agent '${agentId}' not found.`);
1217
- throw await readApiError(response, "List advisories failed");
1218
- }
1219
- return (await response.json());
1172
+ return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/sideband-advisories${qs}`, {}, { notFoundLabel: `Agent '${agentId}'`, fallback: "List advisories failed" });
1220
1173
  }
1221
1174
  /**
1222
1175
  * GET /v1/teams/:id/sideband-advisories
@@ -1231,21 +1184,7 @@ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
1231
1184
  if (opts.since)
1232
1185
  params.set("since", opts.since);
1233
1186
  const qs = params.toString() ? `?${params.toString()}` : "";
1234
- const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-advisories${qs}`);
1235
- const response = await fetchWithAuthRetry(url, async () => ({
1236
- method: "GET",
1237
- headers: { ...(await authHeaders()), Accept: "application/json" },
1238
- }));
1239
- if (!response.ok) {
1240
- if (response.status === 401)
1241
- throw new MnemomApiError(401, "Not authenticated.");
1242
- if (response.status === 403)
1243
- throw new MnemomApiError(403, "Permission denied (need org membership).");
1244
- if (response.status === 404)
1245
- throw new MnemomApiError(404, `Team '${teamId}' not found.`);
1246
- throw await readApiError(response, "List advisories failed");
1247
- }
1248
- return (await response.json());
1187
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/sideband-advisories${qs}`, {}, { notFoundLabel: `Team '${teamId}'`, fallback: "List advisories failed" });
1249
1188
  }
1250
1189
  /**
1251
1190
  * GET /v1/teams/:id/sideband-coverage
@@ -1255,21 +1194,7 @@ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
1255
1194
  * raw rows.
1256
1195
  */
1257
1196
  export async function getTeamSidebandCoverage(teamId) {
1258
- const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-coverage`);
1259
- const response = await fetchWithAuthRetry(url, async () => ({
1260
- method: "GET",
1261
- headers: { ...(await authHeaders()), Accept: "application/json" },
1262
- }));
1263
- if (!response.ok) {
1264
- if (response.status === 401)
1265
- throw new MnemomApiError(401, "Not authenticated.");
1266
- if (response.status === 403)
1267
- throw new MnemomApiError(403, "Permission denied (need org membership).");
1268
- if (response.status === 404)
1269
- throw new MnemomApiError(404, `Team '${teamId}' not found.`);
1270
- throw await readApiError(response, "Coverage fetch failed");
1271
- }
1272
- return (await response.json());
1197
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/sideband-coverage`, {}, { notFoundLabel: `Team '${teamId}'`, fallback: "Coverage fetch failed" });
1273
1198
  }
1274
1199
  /**
1275
1200
  * GET /v1/admin/safe-house/harness-state — fetch the most recent
@@ -1284,7 +1209,7 @@ export async function getSafeHouseHarnessState() {
1284
1209
  }));
1285
1210
  if (!response.ok) {
1286
1211
  if (response.status === 401)
1287
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login`.");
1212
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1288
1213
  if (response.status === 403)
1289
1214
  throw new MnemomApiError(403, "Permission denied — `mnemom validate safe-house` is mnemom-staff only.");
1290
1215
  throw await readApiError(response, "Harness state fetch failed");
@@ -1310,7 +1235,7 @@ function buildSignalListQuery(opts = {}) {
1310
1235
  const qs = params.toString();
1311
1236
  return qs ? `?${qs}` : "";
1312
1237
  }
1313
- async function gFetch(path, init = {}, notFoundLabel) {
1238
+ async function gFetch(path, init = {}, opts = {}) {
1314
1239
  const url = validateUrl(`${API_BASE}${path}`);
1315
1240
  const response = await fetchWithAuthRetry(url, async () => ({
1316
1241
  method: init.method ?? "GET",
@@ -1324,56 +1249,56 @@ async function gFetch(path, init = {}, notFoundLabel) {
1324
1249
  }));
1325
1250
  if (!response.ok) {
1326
1251
  if (response.status === 401)
1327
- throw new MnemomApiError(401, "Not authenticated.");
1252
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1328
1253
  if (response.status === 403)
1329
1254
  throw new MnemomApiError(403, "Permission denied (need org admin / membership).");
1330
- if (response.status === 404 && notFoundLabel)
1331
- throw new MnemomApiError(404, `${notFoundLabel} not found.`);
1332
- throw await readApiError(response, "Request failed");
1255
+ if (response.status === 404 && opts.notFoundLabel)
1256
+ throw new MnemomApiError(404, `${opts.notFoundLabel} not found.`);
1257
+ throw await readApiError(response, opts.fallback ?? "Request failed");
1333
1258
  }
1334
1259
  return (await response.json());
1335
1260
  }
1336
1261
  export async function listGovernanceSignalsForOrg(orgId, opts = {}) {
1337
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Org");
1262
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, { notFoundLabel: "Org" });
1338
1263
  }
1339
1264
  export async function listGovernanceSignalsForTeam(teamId, opts = {}) {
1340
- return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Team");
1265
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, { notFoundLabel: "Team" });
1341
1266
  }
1342
1267
  export async function listGovernanceSignalsForAgent(agentId, opts = {}) {
1343
- return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Agent");
1268
+ return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, { notFoundLabel: "Agent" });
1344
1269
  }
1345
1270
  export async function getGovernanceSignal(id) {
1346
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, "Signal");
1271
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, { notFoundLabel: "Signal" });
1347
1272
  }
1348
1273
  export async function acknowledgeGovernanceSignal(id, body = {}) {
1349
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1274
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Signal" });
1350
1275
  }
1351
1276
  export async function resolveGovernanceSignal(id, body) {
1352
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1277
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Signal" });
1353
1278
  }
1354
1279
  export async function dismissGovernanceSignal(id, body = {}) {
1355
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1280
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Signal" });
1356
1281
  }
1357
1282
  export async function listGovernanceDestinations(orgId) {
1358
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, "Org");
1283
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, { notFoundLabel: "Org" });
1359
1284
  }
1360
1285
  export async function createGovernanceDestination(orgId, body) {
1361
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, "Org");
1286
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Org" });
1362
1287
  }
1363
1288
  export async function deleteGovernanceDestination(orgId, destinationId) {
1364
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, "Destination");
1289
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, { notFoundLabel: "Destination" });
1365
1290
  }
1366
1291
  export async function testGovernanceDestination(orgId, destinationId) {
1367
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, "Destination");
1292
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, { notFoundLabel: "Destination" });
1368
1293
  }
1369
1294
  export async function listGovernanceRules(orgId) {
1370
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, "Org");
1295
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, { notFoundLabel: "Org" });
1371
1296
  }
1372
1297
  export async function createGovernanceRule(orgId, body) {
1373
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, "Org");
1298
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Org" });
1374
1299
  }
1375
1300
  export async function deleteGovernanceRule(orgId, ruleId) {
1376
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, "Rule");
1301
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, { notFoundLabel: "Rule" });
1377
1302
  }
1378
1303
  // ─── api-keys (ADR-049) ─────────────────────────────────────────────────
1379
1304
  /**
@@ -1408,17 +1333,7 @@ export function isLegacyScopeSet(scopes) {
1408
1333
  * GET /v1/api-keys — list the caller's active personal API keys.
1409
1334
  */
1410
1335
  export async function listApiKeys() {
1411
- const url = validateUrl(`${API_BASE}/v1/api-keys`);
1412
- const response = await fetchWithAuthRetry(url, async () => ({
1413
- headers: await authHeaders(),
1414
- }));
1415
- if (!response.ok) {
1416
- if (response.status === 401) {
1417
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1418
- }
1419
- throw await readApiError(response, "Failed to list api keys");
1420
- }
1421
- const data = (await response.json());
1336
+ const data = await gFetch("/v1/api-keys", {}, { fallback: "Failed to list api keys" });
1422
1337
  return data.keys ?? [];
1423
1338
  }
1424
1339
  /**
@@ -1437,7 +1352,7 @@ export async function createApiKey(name, scopes) {
1437
1352
  }));
1438
1353
  if (!response.ok) {
1439
1354
  if (response.status === 401) {
1440
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1355
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1441
1356
  }
1442
1357
  const parsed = await parseApiErrorBody(response);
1443
1358
  const detail = parsed.message || `HTTP ${response.status}`;
@@ -1465,7 +1380,7 @@ export async function rotateApiKey(keyId) {
1465
1380
  }));
1466
1381
  if (!response.ok) {
1467
1382
  if (response.status === 401) {
1468
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1383
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1469
1384
  }
1470
1385
  if (response.status === 404)
1471
1386
  throw new MnemomApiError(404, `Key not found: ${keyId}`);
@@ -1486,7 +1401,7 @@ export async function revokeApiKey(keyId) {
1486
1401
  }));
1487
1402
  if (!response.ok) {
1488
1403
  if (response.status === 401) {
1489
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1404
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1490
1405
  }
1491
1406
  if (response.status === 404)
1492
1407
  throw new MnemomApiError(404, `Key not found: ${keyId}`);
@@ -1514,7 +1429,7 @@ export async function revokeApiKey(keyId) {
1514
1429
  */
1515
1430
  export async function validateLicense(jwt, instanceId, instanceMetadata = {}) {
1516
1431
  const url = validateUrl(`${API_BASE}/v1/license/validate`);
1517
- const response = await fetch(url, {
1432
+ const response = await mnemomFetch(url, {
1518
1433
  method: "POST",
1519
1434
  headers: { "Content-Type": "application/json" },
1520
1435
  body: JSON.stringify({
@@ -1531,3 +1446,33 @@ export async function validateLicense(jwt, instanceId, instanceMetadata = {}) {
1531
1446
  export async function reportRecipeFnFp(recipeId, input) {
1532
1447
  return postApi(`/v1/recipes/${encodeURIComponent(recipeId)}/report`, input);
1533
1448
  }
1449
+ /** The only windows the endpoint accepts (`days` enum in `openapi.json`). */
1450
+ export const USAGE_ALLOWED_DAYS = [7, 30, 90];
1451
+ /**
1452
+ * GET /v1/orgs/:org_id/usage/by-person — consumption by person/provider/model.
1453
+ *
1454
+ * Role-gated to owner/admin/auditor; member and viewer get a real 403.
1455
+ *
1456
+ * Gated by USAGE_ATTRIBUTION_API_ENABLED, which returns **404, not 403**, when
1457
+ * off — deliberately, so a disabled endpoint is indistinguishable from one that
1458
+ * does not exist. That makes 404 ambiguous by design (flag off OR unknown org
1459
+ * OR no access), so no `notFoundLabel` is passed here: a caller must not tell
1460
+ * the user "org not found" on what is most often just the flag being off.
1461
+ */
1462
+ export async function getOrgUsage(orgId, opts = {}) {
1463
+ const params = new URLSearchParams();
1464
+ if (opts.days !== undefined)
1465
+ params.set("days", String(opts.days));
1466
+ if (opts.personId)
1467
+ params.set("person_id", opts.personId);
1468
+ if (opts.provider)
1469
+ params.set("provider", opts.provider);
1470
+ if (opts.model)
1471
+ params.set("model", opts.model);
1472
+ if (opts.limit !== undefined)
1473
+ params.set("limit", String(opts.limit));
1474
+ if (opts.cursor)
1475
+ params.set("cursor", opts.cursor);
1476
+ const qs = params.toString() ? `?${params.toString()}` : "";
1477
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/usage/by-person${qs}`, {}, { fallback: "Failed to fetch usage" });
1478
+ }