@mnemom/mnemom 0.7.2 → 0.9.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/lib/api.d.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  export declare const API_BASE: string;
2
+ /**
3
+ * Generate a fresh Idempotency-Key for a mutation request.
4
+ *
5
+ * The server's beginIdempotentMutation contract (mnemom-api/src/idempotency.ts)
6
+ * requires this header on every PUT/POST/DELETE to a mutation endpoint;
7
+ * without it the server short-circuits with a 400 "Idempotency-Key header is
8
+ * required". Callers may pass an explicit key (for retries that must hit the
9
+ * cached reservation) — when omitted, we mint a UUIDv4.
10
+ */
11
+ export declare function newIdempotencyKey(): string;
2
12
  export interface Agent {
3
13
  id: string;
4
14
  gateway: string;
@@ -7,13 +17,33 @@ export interface Agent {
7
17
  email?: string;
8
18
  created_at: string;
9
19
  }
20
+ /**
21
+ * Per docs.mnemom.ai/api-reference/openapi.json#components/schemas/IntegrityScore:
22
+ *
23
+ * { agent_id, total_traces, verified_traces, violation_count, integrity_score }
24
+ *
25
+ * `integrity_score` is a value in [0, 1].
26
+ *
27
+ * Pre-this-fix the CLI's interface declared `score` / `verified` /
28
+ * `violations` / `last_updated` — none of which exist on the wire. The
29
+ * `mnemom integrity` command rendered every field as `undefined` (and the
30
+ * score as `NaN%`) against any agent. Field names now match the docs
31
+ * verbatim.
32
+ *
33
+ * NOTE — API shape divergence (flagged for a follow-up mnemom-api PR):
34
+ * the RPC path in handleGetIntegrity returns an *array* of one object
35
+ * (Supabase wraps TABLE-returning RPCs as arrays) without `agent_id`;
36
+ * the manual fallback returns the *object* with `agent_id`. The docs
37
+ * canonical is the object shape. We accept both shapes defensively in
38
+ * getIntegrity below so this CLI works against today's prod API and
39
+ * keeps working when the API normalizes to a single shape.
40
+ */
10
41
  export interface IntegrityScore {
11
- agent_id: string;
12
- score: number;
42
+ agent_id?: string;
13
43
  total_traces: number;
14
- verified: number;
15
- violations: number;
16
- last_updated: string;
44
+ verified_traces: number;
45
+ violation_count: number;
46
+ integrity_score: number;
17
47
  }
18
48
  export interface Trace {
19
49
  id: string;
@@ -29,7 +59,9 @@ export interface ApiError {
29
59
  error: string;
30
60
  message: string;
31
61
  }
32
- export declare function postApi<T>(endpoint: string, body: unknown): Promise<T>;
62
+ export declare function postApi<T>(endpoint: string, body: unknown, opts?: {
63
+ idempotencyKey?: string;
64
+ }): Promise<T>;
33
65
  export declare function verifyBinding(agentId: string, keyHash: string): Promise<{
34
66
  bound: boolean;
35
67
  key_prefix: string | null;
@@ -53,7 +85,30 @@ export declare function listAgents(): Promise<AgentListItem[]>;
53
85
  * Note: capped at 100 agents by listAgents().
54
86
  */
55
87
  export declare function getAgentByName(name: string): Promise<AgentListItem | null>;
88
+ /**
89
+ * Fetch the integrity score for an agent.
90
+ *
91
+ * `/v1/integrity/:id` is the public-when-agent-is-public, owner-otherwise
92
+ * data route. The CLI must send the owner's auth header so private claimed
93
+ * agents (the common case for customers and internal users) return 200
94
+ * instead of 401. fetchWithAuthRetry inherits the 401-retry-with-refresh
95
+ * path from PR #200 so a stale local expiresAt heals transparently.
96
+ */
56
97
  export declare function getIntegrity(id: string): Promise<IntegrityScore>;
98
+ /**
99
+ * Fetch recent traces for an agent.
100
+ *
101
+ * The API returns an envelope `{ traces, limit, offset }` (see
102
+ * mnemom-api/src/index.ts:handleGetTraces). Pre-this-fix the CLI typed
103
+ * the response as `Trace[]` and downstream code crashed with "traces is
104
+ * not iterable" because the envelope is not iterable. We unwrap the
105
+ * envelope here and return the bare array so callers (logs.ts) can keep
106
+ * the simpler shape.
107
+ *
108
+ * Sends auth headers — same reasoning as getIntegrity. The pre-PR-A
109
+ * `{ traces: [], private: true }` leaky envelope branch is gone, so we
110
+ * don't need a special case for it.
111
+ */
57
112
  export declare function getTraces(id: string, limit?: number): Promise<Trace[]>;
58
113
  export interface AlignmentCard {
59
114
  card_id?: string;
@@ -93,11 +148,15 @@ export interface CardResponse {
93
148
  updated_at: string;
94
149
  }
95
150
  export declare function getCard(agentId: string): Promise<CardResponse | null>;
96
- export declare function updateCard(agentId: string, cardJson: AlignmentCard): Promise<{
151
+ export declare function updateCard(agentId: string, cardJson: AlignmentCard, opts?: {
152
+ idempotencyKey?: string;
153
+ }): Promise<{
97
154
  updated: boolean;
98
155
  card_id: string;
99
156
  }>;
100
- export declare function reverifyAgent(agentId: string): Promise<{
157
+ export declare function reverifyAgent(agentId: string, opts?: {
158
+ idempotencyKey?: string;
159
+ }): Promise<{
101
160
  reverified: number;
102
161
  }>;
103
162
  export interface PolicyResponse {
@@ -114,7 +173,9 @@ export interface PolicyListResponse {
114
173
  policy: PolicyResponse | null;
115
174
  }
116
175
  export declare function getPolicy(agentId: string): Promise<PolicyListResponse | null>;
117
- export declare function publishPolicy(agentId: string, policyJson: Record<string, unknown>): Promise<{
176
+ export declare function publishPolicy(agentId: string, policyJson: Record<string, unknown>, opts?: {
177
+ idempotencyKey?: string;
178
+ }): Promise<{
118
179
  id: string;
119
180
  version: number;
120
181
  created: boolean;
@@ -131,3 +192,64 @@ export declare function testPolicyHistorical(agentId: string, policyJson: Record
131
192
  skipped: number;
132
193
  };
133
194
  }>;
195
+ /**
196
+ * Fetch the canonical alignment card as YAML (or JSON fallback).
197
+ * Returns the raw response body as a string.
198
+ */
199
+ export declare function getAlignmentCard(agentId: string, format?: "yaml" | "json"): Promise<{
200
+ body: string;
201
+ contentType: string;
202
+ }>;
203
+ /** Hard cap enforced by the API on inbound alignment-card bodies (413 otherwise). */
204
+ export declare const ALIGNMENT_CARD_MAX_BYTES: number;
205
+ /**
206
+ * Shape of a successful PUT response — the canonical card the composer
207
+ * just wrote, serialized as JSON via mnemom-api/src/composition/response.ts:
208
+ * respondCard with `_composition` stripped. We type the fields we display;
209
+ * the rest of the canonical card is allowed (and ignored at this layer).
210
+ *
211
+ * Pre-this-fix the typed return was `{ card_id, composed: boolean }` —
212
+ * a fictional wrapper. The API never returned a `composed` field; the CLI
213
+ * was rendering "Canonical card recomposed" gated on a value that, on the
214
+ * real wire, is always undefined. Drop the lie; reflect the actual shape.
215
+ */
216
+ export interface PublishedCardResponse {
217
+ card_id?: string;
218
+ agent_id?: string;
219
+ card_version?: string;
220
+ issued_at?: string;
221
+ [key: string]: unknown;
222
+ }
223
+ /**
224
+ * Publish (create or update) an alignment card.
225
+ * Accepts YAML or JSON body; set contentType accordingly.
226
+ */
227
+ export declare function putAlignmentCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json", opts?: {
228
+ idempotencyKey?: string;
229
+ }): Promise<PublishedCardResponse>;
230
+ /**
231
+ * Fetch the canonical protection card as YAML (or JSON fallback).
232
+ */
233
+ export declare function getProtectionCard(agentId: string, format?: "yaml" | "json"): Promise<{
234
+ body: string;
235
+ contentType: string;
236
+ }>;
237
+ /** Hard cap enforced by the API on inbound protection-card bodies (413 otherwise). */
238
+ export declare const PROTECTION_CARD_MAX_BYTES: number;
239
+ /**
240
+ * Publish (create or update) a protection card.
241
+ */
242
+ export declare function putProtectionCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json", opts?: {
243
+ idempotencyKey?: string;
244
+ }): Promise<PublishedCardResponse>;
245
+ /**
246
+ * Resolve an agent name or ID to a server agent ID.
247
+ *
248
+ * Resolution order:
249
+ * 1. Explicit agentName parameter (--agent flag)
250
+ * 2. MNEMOM_AGENT environment variable
251
+ *
252
+ * If the value looks like an agent ID (smolt-* or mnm-*), uses it directly.
253
+ * Otherwise resolves the name from the authenticated user's agent list.
254
+ */
255
+ export declare function resolveAgentId(agentName?: string): Promise<string>;
package/dist/lib/api.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { getApiUrl } from "./config.js";
2
- import { resolveAuth } from "./auth.js";
3
+ import { forceRefreshAccessToken, resolveAuth } from "./auth.js";
3
4
  export const API_BASE = getApiUrl();
4
5
  /** Sanitize file-sourced data before use in outbound HTTP requests. */
5
6
  function sanitizeForHttp(data) {
@@ -13,6 +14,18 @@ function validateUrl(url) {
13
14
  }
14
15
  return parsed.href;
15
16
  }
17
+ /**
18
+ * Generate a fresh Idempotency-Key for a mutation request.
19
+ *
20
+ * The server's beginIdempotentMutation contract (mnemom-api/src/idempotency.ts)
21
+ * requires this header on every PUT/POST/DELETE to a mutation endpoint;
22
+ * without it the server short-circuits with a 400 "Idempotency-Key header is
23
+ * required". Callers may pass an explicit key (for retries that must hit the
24
+ * cached reservation) — when omitted, we mint a UUIDv4.
25
+ */
26
+ export function newIdempotencyKey() {
27
+ return randomUUID();
28
+ }
16
29
  async function fetchApi(endpoint) {
17
30
  const url = validateUrl(`${API_BASE}${endpoint}`);
18
31
  const response = await fetch(url);
@@ -25,11 +38,12 @@ async function fetchApi(endpoint) {
25
38
  }
26
39
  return response.json();
27
40
  }
28
- export async function postApi(endpoint, body) {
41
+ export async function postApi(endpoint, body, opts = {}) {
29
42
  const url = validateUrl(`${API_BASE}${endpoint}`);
30
43
  const cred = await resolveAuth();
31
44
  const headers = {
32
45
  "Content-Type": "application/json",
46
+ "Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
33
47
  };
34
48
  if (cred.type === "jwt") {
35
49
  headers["Authorization"] = `Bearer ${cred.token}`;
@@ -74,15 +88,44 @@ async function authHeaders() {
74
88
  return {};
75
89
  }
76
90
  }
91
+ /**
92
+ * Issue an authenticated fetch, and on a 401 response transparently force a
93
+ * token refresh and retry once.
94
+ *
95
+ * We hit this in the wild when the locally-stored expiresAt diverges from
96
+ * the JWT's actual exp claim (e.g. Supabase has been observed reporting
97
+ * expires_in values longer than the JWT's exp). Without this retry,
98
+ * `whoami` cheerfully reports a "valid" token while every authenticated
99
+ * call gets 401 — and the user has no path forward besides
100
+ * `mnemom logout && mnemom login`. The retry heals stale auth files
101
+ * transparently for users who haven't re-logged-in since the
102
+ * computeExpiresAt fix landed.
103
+ *
104
+ * `buildInit` is invoked fresh for each attempt so the retry picks up the
105
+ * new Authorization header from the refreshed token. We do NOT mint a new
106
+ * Idempotency-Key on the retry — the same key keys back into the same
107
+ * server-side reservation by design.
108
+ */
109
+ async function fetchWithAuthRetry(url, buildInit) {
110
+ const first = await fetch(url, await buildInit());
111
+ if (first.status !== 401)
112
+ return first;
113
+ const refreshed = await forceRefreshAccessToken();
114
+ if (!refreshed)
115
+ return first;
116
+ return fetch(url, await buildInit());
117
+ }
77
118
  export async function getAgent(id) {
78
119
  return fetchApi(`/v1/agents/${id}`);
79
120
  }
80
121
  export async function listAgents() {
81
122
  const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
82
- const response = await fetch(url, { headers: await authHeaders() });
123
+ const response = await fetchWithAuthRetry(url, async () => ({
124
+ headers: await authHeaders(),
125
+ }));
83
126
  if (!response.ok) {
84
127
  if (response.status === 401) {
85
- throw new Error("Not authenticated. Run `smoltbot login` or set MNEMOM_API_KEY.");
128
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
86
129
  }
87
130
  const err = await response.json().catch(() => ({ error: "unknown" }));
88
131
  throw new Error(err.message || `Failed to list agents: ${response.status}`);
@@ -112,11 +155,79 @@ export async function getAgentByName(name) {
112
155
  }
113
156
  return null;
114
157
  }
158
+ /**
159
+ * Fetch the integrity score for an agent.
160
+ *
161
+ * `/v1/integrity/:id` is the public-when-agent-is-public, owner-otherwise
162
+ * data route. The CLI must send the owner's auth header so private claimed
163
+ * agents (the common case for customers and internal users) return 200
164
+ * instead of 401. fetchWithAuthRetry inherits the 401-retry-with-refresh
165
+ * path from PR #200 so a stale local expiresAt heals transparently.
166
+ */
115
167
  export async function getIntegrity(id) {
116
- return fetchApi(`/v1/integrity/${id}`);
168
+ const url = validateUrl(`${API_BASE}/v1/integrity/${id}`);
169
+ const response = await fetchWithAuthRetry(url, async () => ({
170
+ headers: await authHeaders(),
171
+ }));
172
+ if (!response.ok) {
173
+ const error = (await response.json().catch(() => ({
174
+ error: "unknown",
175
+ message: response.statusText,
176
+ })));
177
+ throw new Error(error.message || `API request failed: ${response.status}`);
178
+ }
179
+ // Accept both the canonical docs shape (object) and the RPC-wrapped shape
180
+ // (array of one row). The latter is what prod actually returns today on
181
+ // the RPC path; flagged for a follow-up API normalization.
182
+ const body = (await response.json());
183
+ const row = Array.isArray(body) ? (body[0] ?? emptyIntegrityRow(id)) : body;
184
+ // Fill in agent_id when the API didn't (RPC path) so the field is always
185
+ // populated for consumers regardless of which API path served the request.
186
+ if (!row.agent_id)
187
+ row.agent_id = id;
188
+ return row;
189
+ }
190
+ function emptyIntegrityRow(agentId) {
191
+ return {
192
+ agent_id: agentId,
193
+ total_traces: 0,
194
+ verified_traces: 0,
195
+ violation_count: 0,
196
+ integrity_score: 1,
197
+ };
117
198
  }
199
+ /**
200
+ * Fetch recent traces for an agent.
201
+ *
202
+ * The API returns an envelope `{ traces, limit, offset }` (see
203
+ * mnemom-api/src/index.ts:handleGetTraces). Pre-this-fix the CLI typed
204
+ * the response as `Trace[]` and downstream code crashed with "traces is
205
+ * not iterable" because the envelope is not iterable. We unwrap the
206
+ * envelope here and return the bare array so callers (logs.ts) can keep
207
+ * the simpler shape.
208
+ *
209
+ * Sends auth headers — same reasoning as getIntegrity. The pre-PR-A
210
+ * `{ traces: [], private: true }` leaky envelope branch is gone, so we
211
+ * don't need a special case for it.
212
+ */
118
213
  export async function getTraces(id, limit = 10) {
119
- return fetchApi(`/v1/traces?agent_id=${id}&limit=${limit}`);
214
+ const url = validateUrl(`${API_BASE}/v1/traces?agent_id=${id}&limit=${limit}`);
215
+ const response = await fetchWithAuthRetry(url, async () => ({
216
+ headers: await authHeaders(),
217
+ }));
218
+ if (!response.ok) {
219
+ const error = (await response.json().catch(() => ({
220
+ error: "unknown",
221
+ message: response.statusText,
222
+ })));
223
+ throw new Error(error.message || `API request failed: ${response.status}`);
224
+ }
225
+ const data = (await response.json());
226
+ // Accept both the envelope shape (current API) and a bare array (defensive
227
+ // — staging is mid-deploy when a CLI smoke runs against an older API).
228
+ if (Array.isArray(data))
229
+ return data;
230
+ return data.traces ?? [];
120
231
  }
121
232
  export async function getCard(agentId) {
122
233
  try {
@@ -130,11 +241,15 @@ export async function getCard(agentId) {
130
241
  throw error;
131
242
  }
132
243
  }
133
- export async function updateCard(agentId, cardJson) {
244
+ export async function updateCard(agentId, cardJson, opts = {}) {
134
245
  const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/card`);
135
246
  const response = await fetch(url, {
136
247
  method: "PATCH",
137
- headers: { "Content-Type": "application/json", ...(await authHeaders()) },
248
+ headers: {
249
+ "Content-Type": "application/json",
250
+ "Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
251
+ ...(await authHeaders()),
252
+ },
138
253
  body: sanitizeForHttp(JSON.stringify({ card_json: cardJson })),
139
254
  });
140
255
  if (!response.ok) {
@@ -146,11 +261,15 @@ export async function updateCard(agentId, cardJson) {
146
261
  }
147
262
  return response.json();
148
263
  }
149
- export async function reverifyAgent(agentId) {
264
+ export async function reverifyAgent(agentId, opts = {}) {
150
265
  const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/reverify`);
151
266
  const response = await fetch(url, {
152
267
  method: "POST",
153
- headers: { "Content-Type": "application/json", ...(await authHeaders()) },
268
+ headers: {
269
+ "Content-Type": "application/json",
270
+ "Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
271
+ ...(await authHeaders()),
272
+ },
154
273
  });
155
274
  if (!response.ok) {
156
275
  const error = (await response.json().catch(() => ({
@@ -173,11 +292,15 @@ export async function getPolicy(agentId) {
173
292
  throw error;
174
293
  }
175
294
  }
176
- export async function publishPolicy(agentId, policyJson) {
295
+ export async function publishPolicy(agentId, policyJson, opts = {}) {
177
296
  const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/policy`);
178
297
  const response = await fetch(url, {
179
298
  method: "PUT",
180
- headers: { "Content-Type": "application/json", ...(await authHeaders()) },
299
+ headers: {
300
+ "Content-Type": "application/json",
301
+ "Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
302
+ ...(await authHeaders()),
303
+ },
181
304
  body: sanitizeForHttp(JSON.stringify({ policy_json: policyJson })),
182
305
  });
183
306
  if (!response.ok) {
@@ -205,3 +328,166 @@ export async function testPolicyHistorical(agentId, policyJson, limit = 50) {
205
328
  }
206
329
  return response.json();
207
330
  }
331
+ // ============================================================================
332
+ // Unified Card API (UC-4+)
333
+ // ============================================================================
334
+ /**
335
+ * Fetch the canonical alignment card as YAML (or JSON fallback).
336
+ * Returns the raw response body as a string.
337
+ */
338
+ export async function getAlignmentCard(agentId, format = "yaml") {
339
+ const accept = format === "yaml" ? "text/yaml" : "application/json";
340
+ const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/alignment-card`);
341
+ const response = await fetch(url, {
342
+ headers: { Accept: accept, ...(await authHeaders()) },
343
+ });
344
+ if (!response.ok) {
345
+ if (response.status === 404) {
346
+ return { body: "", contentType: "" };
347
+ }
348
+ const error = (await response.json().catch(() => ({
349
+ error: "unknown",
350
+ message: response.statusText,
351
+ })));
352
+ throw new Error(error.message || `Failed to fetch alignment card: ${response.status}`);
353
+ }
354
+ const ct = response.headers.get("content-type") ?? "";
355
+ const body = await response.text();
356
+ return { body, contentType: ct };
357
+ }
358
+ /** Hard cap enforced by the API on inbound alignment-card bodies (413 otherwise). */
359
+ export const ALIGNMENT_CARD_MAX_BYTES = 128 * 1024;
360
+ /**
361
+ * Publish (create or update) an alignment card.
362
+ * Accepts YAML or JSON body; set contentType accordingly.
363
+ */
364
+ export async function putAlignmentCard(agentId, body, contentType = "text/yaml", opts = {}) {
365
+ const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/alignment-card`);
366
+ // Lock in a single Idempotency-Key for this logical mutation. If the auth
367
+ // token is stale and the first attempt returns 401, fetchWithAuthRetry
368
+ // refreshes the token and retries — but the Idempotency-Key must be the
369
+ // same key on both attempts so the server's reservation table sees them as
370
+ // a single mutation, not two competing PUTs.
371
+ const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
372
+ const sanitizedBody = sanitizeForHttp(body);
373
+ const response = await fetchWithAuthRetry(url, async () => ({
374
+ method: "PUT",
375
+ headers: {
376
+ "Content-Type": contentType,
377
+ // The API content-negotiates the response body via Accept (see
378
+ // mnemom-api/src/composition/response.ts:respondYamlJson). We parse the
379
+ // response as JSON below, so we have to ask for JSON explicitly —
380
+ // Node's default Accept: */* would otherwise yield a YAML body and
381
+ // response.json() would crash with "Unexpected token 'a' is not valid
382
+ // JSON" against the canonical card we just wrote.
383
+ Accept: "application/json",
384
+ "Idempotency-Key": idempotencyKey,
385
+ ...(await authHeaders()),
386
+ },
387
+ body: sanitizedBody,
388
+ }));
389
+ if (!response.ok) {
390
+ const error = (await response.json().catch(() => ({
391
+ error: "unknown",
392
+ message: response.statusText,
393
+ })));
394
+ throw new Error(error.message || `Failed to publish alignment card: ${response.status}`);
395
+ }
396
+ return response.json();
397
+ }
398
+ /**
399
+ * Fetch the canonical protection card as YAML (or JSON fallback).
400
+ */
401
+ export async function getProtectionCard(agentId, format = "yaml") {
402
+ const accept = format === "yaml" ? "text/yaml" : "application/json";
403
+ const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/protection-card`);
404
+ const response = await fetch(url, {
405
+ headers: { Accept: accept, ...(await authHeaders()) },
406
+ });
407
+ if (!response.ok) {
408
+ if (response.status === 404) {
409
+ return { body: "", contentType: "" };
410
+ }
411
+ const error = (await response.json().catch(() => ({
412
+ error: "unknown",
413
+ message: response.statusText,
414
+ })));
415
+ throw new Error(error.message || `Failed to fetch protection card: ${response.status}`);
416
+ }
417
+ const ct = response.headers.get("content-type") ?? "";
418
+ const body = await response.text();
419
+ return { body, contentType: ct };
420
+ }
421
+ /** Hard cap enforced by the API on inbound protection-card bodies (413 otherwise). */
422
+ export const PROTECTION_CARD_MAX_BYTES = 64 * 1024;
423
+ /**
424
+ * Publish (create or update) a protection card.
425
+ */
426
+ export async function putProtectionCard(agentId, body, contentType = "text/yaml", opts = {}) {
427
+ const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/protection-card`);
428
+ // See putAlignmentCard for why the Idempotency-Key is computed once outside
429
+ // the retry closure.
430
+ const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
431
+ const sanitizedBody = sanitizeForHttp(body);
432
+ const response = await fetchWithAuthRetry(url, async () => ({
433
+ method: "PUT",
434
+ headers: {
435
+ "Content-Type": contentType,
436
+ // See putAlignmentCard for the rationale — the API negotiates response
437
+ // body format via Accept and we parse the response as JSON below.
438
+ Accept: "application/json",
439
+ "Idempotency-Key": idempotencyKey,
440
+ ...(await authHeaders()),
441
+ },
442
+ body: sanitizedBody,
443
+ }));
444
+ if (!response.ok) {
445
+ const error = (await response.json().catch(() => ({
446
+ error: "unknown",
447
+ message: response.statusText,
448
+ })));
449
+ throw new Error(error.message || `Failed to publish protection card: ${response.status}`);
450
+ }
451
+ return response.json();
452
+ }
453
+ // ============================================================================
454
+ // Agent Resolution (server-side, no local config)
455
+ // ============================================================================
456
+ /**
457
+ * Resolve an agent name or ID to a server agent ID.
458
+ *
459
+ * Resolution order:
460
+ * 1. Explicit agentName parameter (--agent flag)
461
+ * 2. MNEMOM_AGENT environment variable
462
+ *
463
+ * If the value looks like an agent ID (smolt-* or mnm-*), uses it directly.
464
+ * Otherwise resolves the name from the authenticated user's agent list.
465
+ */
466
+ export async function resolveAgentId(agentName) {
467
+ const name = agentName ?? process.env.MNEMOM_AGENT;
468
+ if (!name) {
469
+ console.error("\nAgent required. Use --agent <name> or set MNEMOM_AGENT.\n");
470
+ console.error("List your agents with: mnemom agents\n");
471
+ process.exit(1);
472
+ }
473
+ // If it looks like an agent ID, use directly (no server call needed)
474
+ if (/^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/.test(name)) {
475
+ return name;
476
+ }
477
+ // Resolve name from server (requires auth)
478
+ try {
479
+ const agent = await getAgentByName(name);
480
+ if (agent)
481
+ return agent.id;
482
+ }
483
+ catch (err) {
484
+ const msg = err instanceof Error ? err.message : String(err);
485
+ if (msg.includes("Not authenticated")) {
486
+ console.error(`\nNot authenticated. Run \`mnemom login\` first.\n`);
487
+ process.exit(1);
488
+ }
489
+ }
490
+ console.error(`\nAgent not found: ${name}`);
491
+ console.error("List your agents with: mnemom agents\n");
492
+ process.exit(1);
493
+ }
@@ -1,4 +1,26 @@
1
- import { type AuthTokens } from "./config.js";
1
+ /**
2
+ * Auth credential management.
3
+ *
4
+ * Stores auth tokens in ~/.mnemom/auth.json (UC-9: no more config.json).
5
+ * License JWTs are stored alongside auth tokens.
6
+ */
7
+ export interface AuthTokens {
8
+ accessToken: string;
9
+ refreshToken: string;
10
+ expiresAt: number;
11
+ userId: string;
12
+ email: string;
13
+ }
14
+ export interface AuthStore {
15
+ auth?: AuthTokens;
16
+ licenseJwt?: string;
17
+ }
18
+ export declare function saveAuthTokens(tokens: AuthTokens): void;
19
+ export declare function clearAuthTokens(): void;
20
+ export declare function getAuthInfo(): AuthTokens | null;
21
+ export declare function saveLicenseJwt(jwt: string): void;
22
+ export declare function clearLicenseJwt(): void;
23
+ export declare function getLicenseJwt(): string | null;
2
24
  export type AuthCredential = {
3
25
  type: "jwt";
4
26
  token: string;
@@ -8,32 +30,44 @@ export type AuthCredential = {
8
30
  } | {
9
31
  type: "none";
10
32
  };
33
+ /**
34
+ * Compute the effective expiresAt for a freshly issued access token.
35
+ * Prefers the JWT's own `exp` claim; falls back to `now + expires_in` if the
36
+ * token can't be parsed (e.g. an opaque token).
37
+ */
38
+ export declare function computeExpiresAt(accessToken: string, expiresInSeconds: number): number;
11
39
  /**
12
40
  * Get a valid access token, or null if not authenticated.
13
41
  *
14
42
  * Resolution order:
15
- * 1. SMOLTBOT_TOKEN environment variable (CI / non-interactive)
16
- * 2. Stored token from config (auto-refreshes if expired)
43
+ * 1. MNEMOM_TOKEN environment variable (CI / non-interactive)
44
+ * 2. Stored token from auth store (auto-refreshes if expired)
17
45
  */
18
46
  export declare function getAccessToken(): Promise<string | null>;
47
+ /**
48
+ * Force a refresh of the stored access token regardless of local expiry, and
49
+ * return the new access token (or null if refresh failed).
50
+ *
51
+ * Intended as a 401-recovery hook for callers: if an authenticated request
52
+ * comes back unauthorized despite the local cache claiming a valid token, the
53
+ * cache is stale (clock skew, divergence between expires_in and the JWT's
54
+ * actual exp, or server-side revocation). Force a refresh and retry once.
55
+ */
56
+ export declare function forceRefreshAccessToken(): Promise<string | null>;
19
57
  /**
20
58
  * Get a valid access token or exit with a helpful message.
21
59
  */
22
60
  export declare function requireAccessToken(): Promise<string>;
23
61
  /**
24
- * Get the Mnemom API key from env var or config.
25
- *
26
- * Resolution order:
27
- * 1. MNEMOM_API_KEY environment variable
28
- * 2. Stored mnemomApiKey from config
62
+ * Get the Mnemom API key from env var.
29
63
  */
30
64
  export declare function getMnemomApiKey(): string | null;
31
65
  /**
32
66
  * Resolve the best available auth credential.
33
67
  *
34
68
  * Resolution order:
35
- * 1. JWT (SMOLTBOT_TOKEN env or stored token with auto-refresh)
36
- * 2. API key (MNEMOM_API_KEY env or config mnemomApiKey)
69
+ * 1. JWT (MNEMOM_TOKEN env or stored token with auto-refresh)
70
+ * 2. API key (MNEMOM_API_KEY env)
37
71
  * 3. None
38
72
  */
39
73
  export declare function resolveAuth(): Promise<AuthCredential>;
@@ -44,17 +78,8 @@ export declare function requireAuth(): Promise<AuthCredential & {
44
78
  type: "jwt" | "api-key";
45
79
  }>;
46
80
  /**
47
- * Authenticate via browser-based login flow.
48
- *
49
- * 1. Start a local HTTP server on a random port
50
- * 2. Generate a random `state` nonce for CSRF protection
51
- * 3. Open the browser to the API's CLI login page
52
- * 4. Wait for the login page to POST tokens back to localhost
53
- * 5. Verify state, store tokens, and close the server
81
+ * Check if the user is logged in (has any credential).
54
82
  */
83
+ export declare function isLoggedIn(): Promise<boolean>;
55
84
  export declare function loginWithBrowser(): Promise<AuthTokens>;
56
- /**
57
- * Authenticate with email + password via the API auth proxy.
58
- * Used by --no-browser fallback.
59
- */
60
85
  export declare function loginWithPassword(email: string, password: string): Promise<AuthTokens>;