@mnemom/mnemom 0.8.0 → 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/commands/card.d.ts +16 -5
- package/dist/commands/card.js +476 -89
- package/dist/commands/integrity.js +7 -6
- package/dist/commands/protection.d.ts +12 -4
- package/dist/commands/protection.js +288 -54
- package/dist/commands/status.js +6 -4
- package/dist/index.js +12 -8
- package/dist/lib/api.d.ts +98 -17
- package/dist/lib/api.js +177 -21
- package/dist/lib/auth.d.ts +16 -0
- package/dist/lib/auth.js +53 -3
- package/package.json +1 -1
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
|
|
12
|
-
score: number;
|
|
42
|
+
agent_id?: string;
|
|
13
43
|
total_traces: number;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
@@ -139,14 +200,33 @@ export declare function getAlignmentCard(agentId: string, format?: "yaml" | "jso
|
|
|
139
200
|
body: string;
|
|
140
201
|
contentType: string;
|
|
141
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
|
+
}
|
|
142
223
|
/**
|
|
143
224
|
* Publish (create or update) an alignment card.
|
|
144
225
|
* Accepts YAML or JSON body; set contentType accordingly.
|
|
145
226
|
*/
|
|
146
|
-
export declare function putAlignmentCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}>;
|
|
227
|
+
export declare function putAlignmentCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json", opts?: {
|
|
228
|
+
idempotencyKey?: string;
|
|
229
|
+
}): Promise<PublishedCardResponse>;
|
|
150
230
|
/**
|
|
151
231
|
* Fetch the canonical protection card as YAML (or JSON fallback).
|
|
152
232
|
*/
|
|
@@ -154,13 +234,14 @@ export declare function getProtectionCard(agentId: string, format?: "yaml" | "js
|
|
|
154
234
|
body: string;
|
|
155
235
|
contentType: string;
|
|
156
236
|
}>;
|
|
237
|
+
/** Hard cap enforced by the API on inbound protection-card bodies (413 otherwise). */
|
|
238
|
+
export declare const PROTECTION_CARD_MAX_BYTES: number;
|
|
157
239
|
/**
|
|
158
240
|
* Publish (create or update) a protection card.
|
|
159
241
|
*/
|
|
160
|
-
export declare function putProtectionCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}>;
|
|
242
|
+
export declare function putProtectionCard(agentId: string, body: string, contentType?: "text/yaml" | "application/json", opts?: {
|
|
243
|
+
idempotencyKey?: string;
|
|
244
|
+
}): Promise<PublishedCardResponse>;
|
|
164
245
|
/**
|
|
165
246
|
* Resolve an agent name or ID to a server agent ID.
|
|
166
247
|
*
|
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,12 +88,41 @@ 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
|
|
123
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
124
|
+
headers: await authHeaders(),
|
|
125
|
+
}));
|
|
83
126
|
if (!response.ok) {
|
|
84
127
|
if (response.status === 401) {
|
|
85
128
|
throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
|
|
@@ -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
|
-
|
|
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;
|
|
117
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
|
+
};
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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) {
|
|
@@ -232,17 +355,37 @@ export async function getAlignmentCard(agentId, format = "yaml") {
|
|
|
232
355
|
const body = await response.text();
|
|
233
356
|
return { body, contentType: ct };
|
|
234
357
|
}
|
|
358
|
+
/** Hard cap enforced by the API on inbound alignment-card bodies (413 otherwise). */
|
|
359
|
+
export const ALIGNMENT_CARD_MAX_BYTES = 128 * 1024;
|
|
235
360
|
/**
|
|
236
361
|
* Publish (create or update) an alignment card.
|
|
237
362
|
* Accepts YAML or JSON body; set contentType accordingly.
|
|
238
363
|
*/
|
|
239
|
-
export async function putAlignmentCard(agentId, body, contentType = "text/yaml") {
|
|
364
|
+
export async function putAlignmentCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
240
365
|
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/alignment-card`);
|
|
241
|
-
|
|
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 () => ({
|
|
242
374
|
method: "PUT",
|
|
243
|
-
headers: {
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}));
|
|
246
389
|
if (!response.ok) {
|
|
247
390
|
const error = (await response.json().catch(() => ({
|
|
248
391
|
error: "unknown",
|
|
@@ -275,16 +418,29 @@ export async function getProtectionCard(agentId, format = "yaml") {
|
|
|
275
418
|
const body = await response.text();
|
|
276
419
|
return { body, contentType: ct };
|
|
277
420
|
}
|
|
421
|
+
/** Hard cap enforced by the API on inbound protection-card bodies (413 otherwise). */
|
|
422
|
+
export const PROTECTION_CARD_MAX_BYTES = 64 * 1024;
|
|
278
423
|
/**
|
|
279
424
|
* Publish (create or update) a protection card.
|
|
280
425
|
*/
|
|
281
|
-
export async function putProtectionCard(agentId, body, contentType = "text/yaml") {
|
|
426
|
+
export async function putProtectionCard(agentId, body, contentType = "text/yaml", opts = {}) {
|
|
282
427
|
const url = validateUrl(`${API_BASE}/v1/agents/${agentId}/protection-card`);
|
|
283
|
-
|
|
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 () => ({
|
|
284
433
|
method: "PUT",
|
|
285
|
-
headers: {
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}));
|
|
288
444
|
if (!response.ok) {
|
|
289
445
|
const error = (await response.json().catch(() => ({
|
|
290
446
|
error: "unknown",
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export type AuthCredential = {
|
|
|
30
30
|
} | {
|
|
31
31
|
type: "none";
|
|
32
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;
|
|
33
39
|
/**
|
|
34
40
|
* Get a valid access token, or null if not authenticated.
|
|
35
41
|
*
|
|
@@ -38,6 +44,16 @@ export type AuthCredential = {
|
|
|
38
44
|
* 2. Stored token from auth store (auto-refreshes if expired)
|
|
39
45
|
*/
|
|
40
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>;
|
|
41
57
|
/**
|
|
42
58
|
* Get a valid access token or exit with a helpful message.
|
|
43
59
|
*/
|
package/dist/lib/auth.js
CHANGED
|
@@ -75,6 +75,37 @@ export function getLicenseJwt() {
|
|
|
75
75
|
function sanitizeForHttp(data) {
|
|
76
76
|
return String(data).trim();
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Decode a JWT's `exp` claim (unix seconds), or null if the token can't be
|
|
80
|
+
* parsed. We use the JWT's own exp as the source of truth for expiry rather
|
|
81
|
+
* than `expires_in` returned by the auth endpoint — the two can disagree
|
|
82
|
+
* (Supabase has been observed reporting expires_in values longer than the
|
|
83
|
+
* JWT's actual exp), and a divergence makes `whoami` cheerfully report a
|
|
84
|
+
* "valid" token while every authenticated API call gets 401.
|
|
85
|
+
*/
|
|
86
|
+
function jwtExpSeconds(accessToken) {
|
|
87
|
+
const parts = accessToken.split(".");
|
|
88
|
+
if (parts.length !== 3)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
92
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return payload.exp;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute the effective expiresAt for a freshly issued access token.
|
|
103
|
+
* Prefers the JWT's own `exp` claim; falls back to `now + expires_in` if the
|
|
104
|
+
* token can't be parsed (e.g. an opaque token).
|
|
105
|
+
*/
|
|
106
|
+
export function computeExpiresAt(accessToken, expiresInSeconds) {
|
|
107
|
+
return jwtExpSeconds(accessToken) ?? Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
108
|
+
}
|
|
78
109
|
/**
|
|
79
110
|
* Get a valid access token, or null if not authenticated.
|
|
80
111
|
*
|
|
@@ -100,6 +131,25 @@ export async function getAccessToken() {
|
|
|
100
131
|
return refreshed.accessToken;
|
|
101
132
|
return null;
|
|
102
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Force a refresh of the stored access token regardless of local expiry, and
|
|
136
|
+
* return the new access token (or null if refresh failed).
|
|
137
|
+
*
|
|
138
|
+
* Intended as a 401-recovery hook for callers: if an authenticated request
|
|
139
|
+
* comes back unauthorized despite the local cache claiming a valid token, the
|
|
140
|
+
* cache is stale (clock skew, divergence between expires_in and the JWT's
|
|
141
|
+
* actual exp, or server-side revocation). Force a refresh and retry once.
|
|
142
|
+
*/
|
|
143
|
+
export async function forceRefreshAccessToken() {
|
|
144
|
+
const envToken = process.env.MNEMOM_TOKEN;
|
|
145
|
+
if (envToken)
|
|
146
|
+
return envToken; // env-supplied tokens are not refreshable
|
|
147
|
+
const auth = getAuthInfo();
|
|
148
|
+
if (!auth?.refreshToken)
|
|
149
|
+
return null;
|
|
150
|
+
const refreshed = await refreshAccessToken(auth.refreshToken);
|
|
151
|
+
return refreshed?.accessToken ?? null;
|
|
152
|
+
}
|
|
103
153
|
/**
|
|
104
154
|
* Get a valid access token or exit with a helpful message.
|
|
105
155
|
*/
|
|
@@ -218,7 +268,7 @@ async function startCallbackServer(expectedState) {
|
|
|
218
268
|
const tokens = {
|
|
219
269
|
accessToken: data.access_token,
|
|
220
270
|
refreshToken: data.refresh_token,
|
|
221
|
-
expiresAt:
|
|
271
|
+
expiresAt: computeExpiresAt(data.access_token, data.expires_in),
|
|
222
272
|
userId: data.user_id,
|
|
223
273
|
email: data.user_email,
|
|
224
274
|
};
|
|
@@ -286,7 +336,7 @@ export async function loginWithPassword(email, password) {
|
|
|
286
336
|
const tokens = {
|
|
287
337
|
accessToken: data.access_token,
|
|
288
338
|
refreshToken: data.refresh_token,
|
|
289
|
-
expiresAt:
|
|
339
|
+
expiresAt: computeExpiresAt(data.access_token, data.expires_in),
|
|
290
340
|
userId: data.user.id,
|
|
291
341
|
email: data.user.email,
|
|
292
342
|
};
|
|
@@ -314,7 +364,7 @@ async function refreshAccessToken(refreshToken) {
|
|
|
314
364
|
const tokens = {
|
|
315
365
|
accessToken: data.access_token,
|
|
316
366
|
refreshToken: data.refresh_token,
|
|
317
|
-
expiresAt:
|
|
367
|
+
expiresAt: computeExpiresAt(data.access_token, data.expires_in),
|
|
318
368
|
userId: existing?.userId ?? "",
|
|
319
369
|
email: existing?.email ?? "",
|
|
320
370
|
};
|