@intentgate-app/intentgate 0.3.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/LICENSE +201 -0
- package/README.md +160 -0
- package/dist/index.cjs +661 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +537 -0
- package/dist/index.d.ts +537 -0
- package/dist/index.js +612 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var IntentGateError = class extends Error {
|
|
3
|
+
/** JSON-RPC error code from the gateway, or 0 if client-side. */
|
|
4
|
+
code;
|
|
5
|
+
/** Optional structured payload from the gateway's `error.data`. */
|
|
6
|
+
data;
|
|
7
|
+
constructor(message, opts = {}) {
|
|
8
|
+
super(message, { cause: opts.cause });
|
|
9
|
+
this.name = "IntentGateError";
|
|
10
|
+
this.code = opts.code ?? 0;
|
|
11
|
+
this.data = opts.data;
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Human-friendly string. When `data` is a string, it usually carries
|
|
16
|
+
* the operator-facing reason (e.g. the Rego rule's explanation), so
|
|
17
|
+
* we surface it on `toString`.
|
|
18
|
+
*/
|
|
19
|
+
toString() {
|
|
20
|
+
if (typeof this.data === "string" && this.data.length > 0) {
|
|
21
|
+
return `${this.message}: ${this.data}`;
|
|
22
|
+
}
|
|
23
|
+
return this.message;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var GatewayError = class extends IntentGateError {
|
|
27
|
+
constructor(message, opts) {
|
|
28
|
+
super(message, opts);
|
|
29
|
+
this.name = "GatewayError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ProtocolError = class extends IntentGateError {
|
|
33
|
+
constructor(message, opts) {
|
|
34
|
+
super(message, opts);
|
|
35
|
+
this.name = "ProtocolError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var CapabilityError = class extends IntentGateError {
|
|
39
|
+
constructor(message, opts) {
|
|
40
|
+
super(message, opts);
|
|
41
|
+
this.name = "CapabilityError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var IntentError = class extends IntentGateError {
|
|
45
|
+
constructor(message, opts) {
|
|
46
|
+
super(message, opts);
|
|
47
|
+
this.name = "IntentError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var PolicyError = class extends IntentGateError {
|
|
51
|
+
constructor(message, opts) {
|
|
52
|
+
super(message, opts);
|
|
53
|
+
this.name = "PolicyError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var BudgetError = class extends IntentGateError {
|
|
57
|
+
constructor(message, opts) {
|
|
58
|
+
super(message, opts);
|
|
59
|
+
this.name = "BudgetError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var ProvenanceError = class extends IntentGateError {
|
|
63
|
+
constructor(message, opts) {
|
|
64
|
+
super(message, opts);
|
|
65
|
+
this.name = "ProvenanceError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var CODE_TO_CLASS = {
|
|
69
|
+
[-32010]: CapabilityError,
|
|
70
|
+
[-32011]: IntentError,
|
|
71
|
+
[-32012]: PolicyError,
|
|
72
|
+
[-32013]: BudgetError,
|
|
73
|
+
[-32014]: ProvenanceError
|
|
74
|
+
};
|
|
75
|
+
function forCode(code) {
|
|
76
|
+
return CODE_TO_CLASS[code] ?? ProtocolError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/client.ts
|
|
80
|
+
var TOOLS_CALL_METHOD = "tools/call";
|
|
81
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
82
|
+
var Gateway = class {
|
|
83
|
+
url;
|
|
84
|
+
token;
|
|
85
|
+
timeoutMs;
|
|
86
|
+
fetchImpl;
|
|
87
|
+
nextId = 1;
|
|
88
|
+
constructor(url, opts = {}) {
|
|
89
|
+
if (!url) {
|
|
90
|
+
throw new Error("Gateway: url is required");
|
|
91
|
+
}
|
|
92
|
+
let cleaned = url;
|
|
93
|
+
while (cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
|
|
94
|
+
this.url = cleaned;
|
|
95
|
+
this.token = opts.token;
|
|
96
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
97
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
98
|
+
if (!this.fetchImpl) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"Gateway: no fetch available; pass `fetch` in options or run on Node 18+"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Invoke a tool through the gateway.
|
|
106
|
+
*
|
|
107
|
+
* Resolves with a {@link ToolCallResult} for an allowed call. Throws
|
|
108
|
+
* one of the typed errors (CapabilityError / IntentError /
|
|
109
|
+
* PolicyError / BudgetError / ProtocolError / GatewayError) when
|
|
110
|
+
* the gateway denies, the request fails to reach the gateway, or
|
|
111
|
+
* the response isn't well-formed JSON-RPC.
|
|
112
|
+
*/
|
|
113
|
+
async toolCall(tool, opts = {}) {
|
|
114
|
+
if (!tool) {
|
|
115
|
+
throw new Error("toolCall: tool is required");
|
|
116
|
+
}
|
|
117
|
+
const id = opts.requestId ?? this.nextId++;
|
|
118
|
+
const body = JSON.stringify({
|
|
119
|
+
jsonrpc: "2.0",
|
|
120
|
+
id,
|
|
121
|
+
method: TOOLS_CALL_METHOD,
|
|
122
|
+
params: {
|
|
123
|
+
name: tool,
|
|
124
|
+
arguments: opts.arguments ?? {}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const headers = {
|
|
128
|
+
"Content-Type": "application/json"
|
|
129
|
+
};
|
|
130
|
+
if (this.token) {
|
|
131
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
132
|
+
}
|
|
133
|
+
if (opts.intentPrompt) {
|
|
134
|
+
headers["X-Intent-Prompt"] = opts.intentPrompt;
|
|
135
|
+
}
|
|
136
|
+
if (opts.memoryProvenance && opts.memoryProvenance.length > 0) {
|
|
137
|
+
if (!opts.memoryStore) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"toolCall: memoryProvenance is non-empty but memoryStore is undefined; supply a MemoryStore so the SDK can look up the envelopes"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const wireEntries = opts.memoryStore.provenanceFor(opts.memoryProvenance);
|
|
143
|
+
headers["X-Intent-Memory-Provenance"] = Buffer.from(JSON.stringify(wireEntries)).toString(
|
|
144
|
+
"base64url"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
149
|
+
let resp;
|
|
150
|
+
try {
|
|
151
|
+
resp = await this.fetchImpl(`${this.url}/v1/mcp`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body,
|
|
154
|
+
headers,
|
|
155
|
+
signal: controller.signal
|
|
156
|
+
});
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
const isAbort = cause instanceof Error && cause.name === "AbortError";
|
|
159
|
+
const msg = isAbort ? `gateway timed out after ${this.timeoutMs}ms` : `transport error reaching gateway: ${stringifyCause(cause)}`;
|
|
160
|
+
throw new GatewayError(msg, { cause });
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
}
|
|
164
|
+
if (!resp.ok) {
|
|
165
|
+
const text = await safeText(resp);
|
|
166
|
+
throw new GatewayError(`gateway returned HTTP ${resp.status}`, {
|
|
167
|
+
data: text || resp.statusText
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
let payload;
|
|
171
|
+
try {
|
|
172
|
+
payload = await resp.json();
|
|
173
|
+
} catch (cause) {
|
|
174
|
+
throw new GatewayError("non-JSON response from gateway", { cause });
|
|
175
|
+
}
|
|
176
|
+
return parseResponse(payload);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function stringifyCause(cause) {
|
|
180
|
+
if (cause instanceof Error) return cause.message;
|
|
181
|
+
return String(cause);
|
|
182
|
+
}
|
|
183
|
+
async function safeText(resp) {
|
|
184
|
+
try {
|
|
185
|
+
const t = await resp.text();
|
|
186
|
+
return t.slice(0, 500);
|
|
187
|
+
} catch {
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function parseResponse(payload) {
|
|
192
|
+
if (!isObject(payload)) {
|
|
193
|
+
throw new ProtocolError("response is not a JSON object");
|
|
194
|
+
}
|
|
195
|
+
const err = payload["error"];
|
|
196
|
+
if (err != null) {
|
|
197
|
+
if (!isObject(err)) {
|
|
198
|
+
throw new ProtocolError("error field is not an object");
|
|
199
|
+
}
|
|
200
|
+
const code = typeof err["code"] === "number" ? err["code"] : 0;
|
|
201
|
+
const message = typeof err["message"] === "string" ? err["message"] : "gateway error";
|
|
202
|
+
const data = err["data"];
|
|
203
|
+
const Cls = forCode(code);
|
|
204
|
+
throw new Cls(message, { code, data });
|
|
205
|
+
}
|
|
206
|
+
const result = payload["result"];
|
|
207
|
+
if (!isObject(result)) {
|
|
208
|
+
throw new ProtocolError("response missing 'result' object", { data: payload });
|
|
209
|
+
}
|
|
210
|
+
const rawContent = Array.isArray(result["content"]) ? result["content"] : [];
|
|
211
|
+
const content = [];
|
|
212
|
+
for (const b of rawContent) {
|
|
213
|
+
if (!isObject(b)) continue;
|
|
214
|
+
content.push({
|
|
215
|
+
type: typeof b["type"] === "string" ? b["type"] : "",
|
|
216
|
+
text: typeof b["text"] === "string" ? b["text"] : void 0
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
let intentgate = null;
|
|
220
|
+
const ig = result["_intentgate"];
|
|
221
|
+
if (isObject(ig)) {
|
|
222
|
+
intentgate = {
|
|
223
|
+
decision: typeof ig["decision"] === "string" ? ig["decision"] : "",
|
|
224
|
+
reason: typeof ig["reason"] === "string" ? ig["reason"] : "",
|
|
225
|
+
check: typeof ig["check"] === "string" ? ig["check"] : "",
|
|
226
|
+
latencyMs: typeof ig["latency_ms"] === "number" ? ig["latency_ms"] : 0
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
content,
|
|
231
|
+
isError: result["isError"] === true,
|
|
232
|
+
intentgate
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function isObject(v) {
|
|
236
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/capability.ts
|
|
240
|
+
import { createHmac } from "crypto";
|
|
241
|
+
var CaveatType = {
|
|
242
|
+
EXPIRY: "exp",
|
|
243
|
+
TOOL_ALLOW: "tool_allow",
|
|
244
|
+
TOOL_DENY: "tool_deny",
|
|
245
|
+
AGENT_LOCK: "agent_lock",
|
|
246
|
+
MAX_CALLS: "max_calls"
|
|
247
|
+
};
|
|
248
|
+
var AttenuationError = class extends Error {
|
|
249
|
+
constructor(message, opts) {
|
|
250
|
+
super(message, { cause: opts?.cause });
|
|
251
|
+
this.name = "AttenuationError";
|
|
252
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
function b64urlDecode(s) {
|
|
256
|
+
const standard = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
257
|
+
const pad = standard.length % 4 === 0 ? "" : "=".repeat(4 - standard.length % 4);
|
|
258
|
+
try {
|
|
259
|
+
return new Uint8Array(Buffer.from(standard + pad, "base64"));
|
|
260
|
+
} catch (cause) {
|
|
261
|
+
throw new AttenuationError(`invalid base64url: ${stringifyCause2(cause)}`, { cause });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function b64urlEncode(b) {
|
|
265
|
+
return Buffer.from(b).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
266
|
+
}
|
|
267
|
+
function canonicalCaveatBytes(c) {
|
|
268
|
+
const obj = {};
|
|
269
|
+
obj["t"] = c.type;
|
|
270
|
+
if (c.tools && c.tools.length > 0) {
|
|
271
|
+
obj["tools"] = [...c.tools];
|
|
272
|
+
}
|
|
273
|
+
if (c.agent) {
|
|
274
|
+
obj["agent"] = c.agent;
|
|
275
|
+
}
|
|
276
|
+
if (c.expiry) {
|
|
277
|
+
obj["exp"] = Math.trunc(c.expiry);
|
|
278
|
+
}
|
|
279
|
+
if (c.maxCalls) {
|
|
280
|
+
obj["max_calls"] = Math.trunc(c.maxCalls);
|
|
281
|
+
}
|
|
282
|
+
const json = JSON.stringify(obj);
|
|
283
|
+
return new TextEncoder().encode(json);
|
|
284
|
+
}
|
|
285
|
+
function decodeToken(token) {
|
|
286
|
+
const raw = b64urlDecode(token);
|
|
287
|
+
let parsed;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
|
|
290
|
+
} catch (cause) {
|
|
291
|
+
throw new AttenuationError(`token JSON is malformed: ${stringifyCause2(cause)}`, {
|
|
292
|
+
cause
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
296
|
+
throw new AttenuationError("token JSON is not an object");
|
|
297
|
+
}
|
|
298
|
+
return parsed;
|
|
299
|
+
}
|
|
300
|
+
function attenuate(token, opts = {}) {
|
|
301
|
+
const parsed = decodeToken(token);
|
|
302
|
+
if (typeof parsed["sig"] !== "string") {
|
|
303
|
+
throw new AttenuationError("token is missing 'sig' field");
|
|
304
|
+
}
|
|
305
|
+
if (!Array.isArray(parsed["cav"])) {
|
|
306
|
+
throw new AttenuationError("token is missing 'cav' field");
|
|
307
|
+
}
|
|
308
|
+
if (typeof parsed["root_jti"] !== "string" || parsed["root_jti"] === "") {
|
|
309
|
+
throw new AttenuationError(
|
|
310
|
+
"token has no root_jti (was it minted by gateway < v0.7?)"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (typeof parsed["tenant"] !== "string" || parsed["tenant"] === "") {
|
|
314
|
+
throw new AttenuationError(
|
|
315
|
+
"token has no tenant (was it minted by gateway < v0.9?)"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const cavs = [...parsed["cav"]];
|
|
319
|
+
let sig = b64urlDecode(parsed["sig"]);
|
|
320
|
+
const newCaveats = [];
|
|
321
|
+
if (opts.addTools && opts.addTools.length > 0) {
|
|
322
|
+
newCaveats.push({ type: CaveatType.TOOL_ALLOW, tools: [...opts.addTools] });
|
|
323
|
+
}
|
|
324
|
+
if (opts.denyTools && opts.denyTools.length > 0) {
|
|
325
|
+
newCaveats.push({ type: CaveatType.TOOL_DENY, tools: [...opts.denyTools] });
|
|
326
|
+
}
|
|
327
|
+
if (opts.maxCalls !== void 0) {
|
|
328
|
+
if (opts.maxCalls < 0) {
|
|
329
|
+
throw new AttenuationError("maxCalls must be >= 0");
|
|
330
|
+
}
|
|
331
|
+
newCaveats.push({ type: CaveatType.MAX_CALLS, maxCalls: Math.trunc(opts.maxCalls) });
|
|
332
|
+
}
|
|
333
|
+
if (opts.expiresAt !== void 0 || opts.expiresInSeconds !== void 0) {
|
|
334
|
+
const exp = opts.expiresAt ?? Math.floor(Date.now() / 1e3) + (opts.expiresInSeconds ?? 0);
|
|
335
|
+
newCaveats.push({ type: CaveatType.EXPIRY, expiry: Math.trunc(exp) });
|
|
336
|
+
}
|
|
337
|
+
if (opts.extra && opts.extra.length > 0) {
|
|
338
|
+
newCaveats.push(...opts.extra);
|
|
339
|
+
}
|
|
340
|
+
if (newCaveats.length === 0) {
|
|
341
|
+
throw new AttenuationError(
|
|
342
|
+
"attenuate() requires at least one narrowing argument (addTools, denyTools, maxCalls, expiresInSeconds, expiresAt, or extra)"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
for (const c of newCaveats) {
|
|
346
|
+
const cb = canonicalCaveatBytes(c);
|
|
347
|
+
sig = new Uint8Array(createHmac("sha256", sig).update(cb).digest());
|
|
348
|
+
cavs.push(JSON.parse(new TextDecoder().decode(cb)));
|
|
349
|
+
}
|
|
350
|
+
const child = { ...parsed };
|
|
351
|
+
child["cav"] = cavs;
|
|
352
|
+
child["sig"] = b64urlEncode(sig);
|
|
353
|
+
return b64urlEncode(new TextEncoder().encode(JSON.stringify(child)));
|
|
354
|
+
}
|
|
355
|
+
function stringifyCause2(cause) {
|
|
356
|
+
if (cause instanceof Error) return cause.message;
|
|
357
|
+
return String(cause);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/memory.ts
|
|
361
|
+
import { createHmac as createHmac2, createHash, hkdfSync, timingSafeEqual, randomUUID } from "crypto";
|
|
362
|
+
var DERIVATION_INFO = Buffer.from("intentgate-memory-v1");
|
|
363
|
+
var SESSION_KEY_SIZE = 32;
|
|
364
|
+
var HASH_SIZE = 32;
|
|
365
|
+
var ZERO_HASH = Buffer.alloc(HASH_SIZE);
|
|
366
|
+
var MemoryProvenanceError = class extends Error {
|
|
367
|
+
constructor(message) {
|
|
368
|
+
super(message);
|
|
369
|
+
this.name = "MemoryProvenanceError";
|
|
370
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
function deriveSessionKey(masterKey, sessionId) {
|
|
374
|
+
if (!masterKey || masterKey.length === 0) {
|
|
375
|
+
throw new Error("deriveSessionKey: masterKey is empty");
|
|
376
|
+
}
|
|
377
|
+
if (!sessionId) {
|
|
378
|
+
throw new Error("deriveSessionKey: sessionId is empty");
|
|
379
|
+
}
|
|
380
|
+
const out = hkdfSync(
|
|
381
|
+
"sha256",
|
|
382
|
+
masterKey,
|
|
383
|
+
Buffer.from(sessionId, "utf8"),
|
|
384
|
+
DERIVATION_INFO,
|
|
385
|
+
SESSION_KEY_SIZE
|
|
386
|
+
);
|
|
387
|
+
return Buffer.from(out);
|
|
388
|
+
}
|
|
389
|
+
function canonical(env) {
|
|
390
|
+
const sid = Buffer.from(env.sessionId, "utf8");
|
|
391
|
+
const eid = Buffer.from(env.id, "utf8");
|
|
392
|
+
const total = 4 + sid.length + 4 + eid.length + 8 + 4 + env.prevHash.length + 4 + env.data.length;
|
|
393
|
+
const out = Buffer.alloc(total);
|
|
394
|
+
let off = 0;
|
|
395
|
+
out.writeUInt32BE(sid.length, off);
|
|
396
|
+
off += 4;
|
|
397
|
+
sid.copy(out, off);
|
|
398
|
+
off += sid.length;
|
|
399
|
+
out.writeUInt32BE(eid.length, off);
|
|
400
|
+
off += 4;
|
|
401
|
+
eid.copy(out, off);
|
|
402
|
+
off += eid.length;
|
|
403
|
+
out.writeBigUInt64BE(BigInt(env.timestamp) & 0xffffffffffffffffn, off);
|
|
404
|
+
off += 8;
|
|
405
|
+
out.writeUInt32BE(env.prevHash.length, off);
|
|
406
|
+
off += 4;
|
|
407
|
+
env.prevHash.copy(out, off);
|
|
408
|
+
off += env.prevHash.length;
|
|
409
|
+
out.writeUInt32BE(env.data.length, off);
|
|
410
|
+
off += 4;
|
|
411
|
+
env.data.copy(out, off);
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
function sign(sessionKey, env) {
|
|
415
|
+
if (!sessionKey || sessionKey.length === 0) {
|
|
416
|
+
throw new Error("sign: sessionKey is empty");
|
|
417
|
+
}
|
|
418
|
+
const mac = createHmac2("sha256", sessionKey);
|
|
419
|
+
mac.update(canonical(env));
|
|
420
|
+
return { ...env, hmac: mac.digest() };
|
|
421
|
+
}
|
|
422
|
+
function verify(sessionKey, env) {
|
|
423
|
+
if (!sessionKey || sessionKey.length === 0) {
|
|
424
|
+
throw new Error("verify: sessionKey is empty");
|
|
425
|
+
}
|
|
426
|
+
if (env.hmac.length !== HASH_SIZE) {
|
|
427
|
+
throw new MemoryProvenanceError(
|
|
428
|
+
`hmac field is ${env.hmac.length} bytes; expected ${HASH_SIZE}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const expected = createHmac2("sha256", sessionKey).update(canonical(env)).digest();
|
|
432
|
+
if (!timingSafeEqual(expected, env.hmac)) {
|
|
433
|
+
throw new MemoryProvenanceError("hmac mismatch");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function verifyChain(sessionKey, chain) {
|
|
437
|
+
if (chain.length === 0) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
for (let i = 0; i < chain.length; i++) {
|
|
441
|
+
const env = chain[i];
|
|
442
|
+
if (!env) continue;
|
|
443
|
+
try {
|
|
444
|
+
verify(sessionKey, env);
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (e instanceof MemoryProvenanceError) {
|
|
447
|
+
throw new MemoryProvenanceError(`entry ${i}: ${e.message}`);
|
|
448
|
+
}
|
|
449
|
+
throw e;
|
|
450
|
+
}
|
|
451
|
+
let expectedPrev;
|
|
452
|
+
if (i === 0) {
|
|
453
|
+
expectedPrev = ZERO_HASH;
|
|
454
|
+
} else {
|
|
455
|
+
const prev = chain[i - 1];
|
|
456
|
+
if (!prev) continue;
|
|
457
|
+
expectedPrev = createHash("sha256").update(canonical(prev)).digest();
|
|
458
|
+
}
|
|
459
|
+
if (!timingSafeEqual(expectedPrev, env.prevHash)) {
|
|
460
|
+
throw new MemoryProvenanceError(
|
|
461
|
+
`entry ${i}: prev_hash does not match previous entry's canonical hash`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
var MemoryStore = class {
|
|
467
|
+
sessionId;
|
|
468
|
+
key;
|
|
469
|
+
fallback = /* @__PURE__ */ new Map();
|
|
470
|
+
writeHook;
|
|
471
|
+
readHook;
|
|
472
|
+
chainHead = ZERO_HASH;
|
|
473
|
+
/**
|
|
474
|
+
* @param sessionId The `jti` of the capability token this store is bound to.
|
|
475
|
+
* Used as the HKDF salt to derive the signing key.
|
|
476
|
+
* @param memorySigningKey The 32-byte signing key returned by
|
|
477
|
+
* `POST /v1/admin/mint` when `with_memory_signing_key: true`.
|
|
478
|
+
* @param options Optional `writeHook` / `readHook` callables; when
|
|
479
|
+
* absent the wrapper uses an in-memory Map fallback.
|
|
480
|
+
*/
|
|
481
|
+
constructor(sessionId, memorySigningKey, options = {}) {
|
|
482
|
+
if (!sessionId) {
|
|
483
|
+
throw new Error("MemoryStore: sessionId is required");
|
|
484
|
+
}
|
|
485
|
+
if (memorySigningKey.length !== SESSION_KEY_SIZE) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`MemoryStore: memorySigningKey must be ${SESSION_KEY_SIZE} bytes, got ${memorySigningKey.length}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
this.sessionId = sessionId;
|
|
491
|
+
this.key = Buffer.from(memorySigningKey);
|
|
492
|
+
this.writeHook = options.writeHook;
|
|
493
|
+
this.readHook = options.readHook;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Sign `data` into a new envelope and store it. Returns the entry
|
|
497
|
+
* ID, which the caller passes to `Gateway.toolCall` via the
|
|
498
|
+
* `memoryProvenance` list.
|
|
499
|
+
*
|
|
500
|
+
* `data` may be a Buffer, a string (utf-8 encoded), or any JSON-
|
|
501
|
+
* serializable value (encoded with sorted keys + no whitespace so
|
|
502
|
+
* equivalent inputs produce identical envelope bytes).
|
|
503
|
+
*/
|
|
504
|
+
write(data) {
|
|
505
|
+
let payload;
|
|
506
|
+
if (Buffer.isBuffer(data)) {
|
|
507
|
+
payload = data;
|
|
508
|
+
} else if (typeof data === "string") {
|
|
509
|
+
payload = Buffer.from(data, "utf8");
|
|
510
|
+
} else {
|
|
511
|
+
payload = Buffer.from(stableStringify(data), "utf8");
|
|
512
|
+
}
|
|
513
|
+
const entryId = randomUUID().replaceAll("-", "");
|
|
514
|
+
const env = sign(this.key, {
|
|
515
|
+
id: entryId,
|
|
516
|
+
sessionId: this.sessionId,
|
|
517
|
+
timestamp: Date.now(),
|
|
518
|
+
data: payload,
|
|
519
|
+
prevHash: this.chainHead
|
|
520
|
+
});
|
|
521
|
+
if (this.writeHook !== void 0) {
|
|
522
|
+
this.writeHook(entryId, env);
|
|
523
|
+
} else {
|
|
524
|
+
this.fallback.set(entryId, env);
|
|
525
|
+
}
|
|
526
|
+
this.chainHead = createHash("sha256").update(canonical(env)).digest();
|
|
527
|
+
return entryId;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Fetch and verify the envelope identified by `entryId`.
|
|
531
|
+
*
|
|
532
|
+
* @throws if the entry is missing (`Error`) or if the HMAC fails
|
|
533
|
+
* ({@link MemoryProvenanceError}, indicating the entry was
|
|
534
|
+
* tampered with after writing).
|
|
535
|
+
*/
|
|
536
|
+
read(entryId) {
|
|
537
|
+
let env;
|
|
538
|
+
if (this.readHook !== void 0) {
|
|
539
|
+
env = this.readHook(entryId);
|
|
540
|
+
} else {
|
|
541
|
+
env = this.fallback.get(entryId);
|
|
542
|
+
if (env === void 0) {
|
|
543
|
+
throw new Error(`MemoryStore: entry ${entryId} not found`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
verify(this.key, env);
|
|
547
|
+
return env;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Build the wire-format provenance entries for a tool call. Each
|
|
551
|
+
* entry is verified before inclusion — if any envelope was tampered
|
|
552
|
+
* with at the storage layer, {@link MemoryProvenanceError} is
|
|
553
|
+
* raised here rather than at the gateway.
|
|
554
|
+
*
|
|
555
|
+
* The returned objects use base64url (no padding) encoding for byte
|
|
556
|
+
* fields — same shape the Go gateway parses.
|
|
557
|
+
*/
|
|
558
|
+
provenanceFor(entryIds) {
|
|
559
|
+
return entryIds.map((eid) => {
|
|
560
|
+
const env = this.read(eid);
|
|
561
|
+
return {
|
|
562
|
+
id: env.id,
|
|
563
|
+
session_id: env.sessionId,
|
|
564
|
+
ts: env.timestamp,
|
|
565
|
+
data: env.data.toString("base64url"),
|
|
566
|
+
prev_hash: env.prevHash.toString("base64url"),
|
|
567
|
+
hmac: env.hmac.toString("base64url")
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/** Number of entries in the fallback in-memory store. */
|
|
572
|
+
get size() {
|
|
573
|
+
return this.fallback.size;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
function stableStringify(value) {
|
|
577
|
+
if (value === null || typeof value !== "object") {
|
|
578
|
+
return JSON.stringify(value);
|
|
579
|
+
}
|
|
580
|
+
if (Array.isArray(value)) {
|
|
581
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
582
|
+
}
|
|
583
|
+
const obj = value;
|
|
584
|
+
const keys = Object.keys(obj).sort();
|
|
585
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
586
|
+
}
|
|
587
|
+
export {
|
|
588
|
+
AttenuationError,
|
|
589
|
+
BudgetError,
|
|
590
|
+
CapabilityError,
|
|
591
|
+
CaveatType,
|
|
592
|
+
Gateway,
|
|
593
|
+
GatewayError,
|
|
594
|
+
IntentError,
|
|
595
|
+
IntentGateError,
|
|
596
|
+
MemoryProvenanceError,
|
|
597
|
+
MemoryStore,
|
|
598
|
+
PolicyError,
|
|
599
|
+
ProtocolError,
|
|
600
|
+
ProvenanceError,
|
|
601
|
+
SESSION_KEY_SIZE,
|
|
602
|
+
ZERO_HASH,
|
|
603
|
+
attenuate,
|
|
604
|
+
canonical,
|
|
605
|
+
decodeToken,
|
|
606
|
+
deriveSessionKey,
|
|
607
|
+
forCode,
|
|
608
|
+
sign,
|
|
609
|
+
verify,
|
|
610
|
+
verifyChain
|
|
611
|
+
};
|
|
612
|
+
//# sourceMappingURL=index.js.map
|