@kya-brasil/agent-tools 0.3.0 → 0.4.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/README.md +15 -0
- package/dist/auth.d.ts +64 -0
- package/dist/auth.js +111 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,21 @@ await fetch(url, { method: "POST", headers: { ...headers, "content-type": "appli
|
|
|
69
69
|
|
|
70
70
|
Keys: `{ kind: "evm" | "erc8004", privateKey }` or `{ kind: "web-bot-auth", privateKey: CryptoKey | pkcs8 }`.
|
|
71
71
|
|
|
72
|
+
### Agent to agent (mutual)
|
|
73
|
+
|
|
74
|
+
The callee signs its response, bound to the request; the caller verifies who answered.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// callee (inside a handler, after auth.authenticate succeeded)
|
|
78
|
+
const headers = await signResponse({ credentialId: myCredentialId, key: myKey, requestSignature: request.headers.get("kya-signature")!, body });
|
|
79
|
+
return new Response(body, { headers: { ...headers, "content-type": "application/json" } });
|
|
80
|
+
|
|
81
|
+
// caller
|
|
82
|
+
const me = createKyaAgent({ credentialId: myCredentialId, key: myKey });
|
|
83
|
+
const { response, callee } = await me.fetch("https://beta.com.br/orders", { method: "POST", body });
|
|
84
|
+
if (!callee?.ok) throw new Error("unsigned or invalid callee"); // callee.kya: principal, mandate, status
|
|
85
|
+
```
|
|
86
|
+
|
|
72
87
|
## Company: give your own agent a credential
|
|
73
88
|
|
|
74
89
|
```ts
|
package/dist/auth.d.ts
CHANGED
|
@@ -97,6 +97,13 @@ export interface KyaAuth {
|
|
|
97
97
|
headers: Headers | Record<string, string | string[] | undefined>;
|
|
98
98
|
body?: string | ArrayBuffer | Uint8Array | null;
|
|
99
99
|
}): Promise<KyaAuthResult>;
|
|
100
|
+
/** Low level: verify a signature by a credential's agent key over a text you built (signed responses use it). */
|
|
101
|
+
verifySigned(p: {
|
|
102
|
+
credentialId: string;
|
|
103
|
+
text: string;
|
|
104
|
+
signature: string;
|
|
105
|
+
body?: KyaContext["body"];
|
|
106
|
+
}): Promise<KyaAuthResult>;
|
|
100
107
|
/** Hono middleware: sets c.set("kya", ctx) or answers 401/403 JSON. */
|
|
101
108
|
hono(): (c: HonoLikeContext, next: () => Promise<void>) => Promise<Response | void>;
|
|
102
109
|
/** Express middleware: sets req.kya or answers 401/403 JSON. Verifies the body when req.body is a string or Buffer. */
|
|
@@ -128,3 +135,60 @@ export interface ExpressLikeResponse {
|
|
|
128
135
|
json: (body: unknown) => unknown;
|
|
129
136
|
}
|
|
130
137
|
export declare function createKyaAuth(opts?: KyaAuthOptions): KyaAuth;
|
|
138
|
+
export declare const KYA_REPLY_HEADER = "kya-in-reply-to";
|
|
139
|
+
export declare function responseText(p: {
|
|
140
|
+
credentialId: string;
|
|
141
|
+
inReplyTo: string;
|
|
142
|
+
created: number;
|
|
143
|
+
expires: number;
|
|
144
|
+
contentSha256?: string | null;
|
|
145
|
+
}): string;
|
|
146
|
+
/** Callee side: headers that prove who answered, bound to the request signature it replies to. */
|
|
147
|
+
export declare function signResponse(p: {
|
|
148
|
+
credentialId: string;
|
|
149
|
+
key: SigningKey;
|
|
150
|
+
requestSignature: string;
|
|
151
|
+
body?: string | ArrayBuffer | Uint8Array | null;
|
|
152
|
+
ttlSeconds?: number;
|
|
153
|
+
now?: Date;
|
|
154
|
+
}): Promise<Record<string, string>>;
|
|
155
|
+
export type KyaResponseResult = {
|
|
156
|
+
ok: true;
|
|
157
|
+
kya: Omit<KyaContext, "check" | "allowed"> & {
|
|
158
|
+
check: KyaContext["check"];
|
|
159
|
+
allowed: KyaContext["allowed"];
|
|
160
|
+
};
|
|
161
|
+
} | {
|
|
162
|
+
ok: false;
|
|
163
|
+
error: string;
|
|
164
|
+
message: string;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Caller side: verify that a response was signed by an active KYA credential and answers *this* request.
|
|
168
|
+
* `requestSignature` is the KYA-Signature header the caller sent.
|
|
169
|
+
*/
|
|
170
|
+
export declare function verifyResponse(auth: KyaAuth, response: Response, requestSignature: string, opts?: {
|
|
171
|
+
body?: string | ArrayBuffer | Uint8Array | null;
|
|
172
|
+
maxSkewSeconds?: number;
|
|
173
|
+
}): Promise<KyaResponseResult>;
|
|
174
|
+
/**
|
|
175
|
+
* Caller side, all in one: a fetch that signs the request with your credential and, when the other side
|
|
176
|
+
* answers with KYA headers, verifies them. `callee` is null when the response was not signed.
|
|
177
|
+
*/
|
|
178
|
+
export declare function createKyaAgent(p: {
|
|
179
|
+
credentialId: string;
|
|
180
|
+
key: SigningKey;
|
|
181
|
+
auth?: KyaAuth;
|
|
182
|
+
authOptions?: KyaAuthOptions;
|
|
183
|
+
fetch?: typeof fetch;
|
|
184
|
+
ttlSeconds?: number;
|
|
185
|
+
}): {
|
|
186
|
+
auth: KyaAuth;
|
|
187
|
+
headers: (method: string, url: string, body?: string | ArrayBuffer | Uint8Array | null) => Promise<Record<string, string>>;
|
|
188
|
+
fetch(url: string, init?: RequestInit & {
|
|
189
|
+
body?: string | ArrayBuffer | Uint8Array | null;
|
|
190
|
+
}): Promise<{
|
|
191
|
+
response: Response;
|
|
192
|
+
callee: KyaResponseResult | null;
|
|
193
|
+
}>;
|
|
194
|
+
};
|
package/dist/auth.js
CHANGED
|
@@ -193,9 +193,12 @@ export function createKyaAuth(opts = {}) {
|
|
|
193
193
|
return status === 404 ? fail(401, "KYA_CREDENTIAL_UNKNOWN", "unknown credential_id") : fail(502, "KYA_ISSUER_UNAVAILABLE", e.message);
|
|
194
194
|
}
|
|
195
195
|
const text = requestText({ credentialId, method: p.method, url: p.url, created, expires, contentSha256: contentHeader });
|
|
196
|
+
return finish(credentialId, v, text, signature, bodyState);
|
|
197
|
+
}
|
|
198
|
+
async function finish(credentialId, v, text, signature, bodyState) {
|
|
196
199
|
const sig = await verifySignature(v.agent, text, signature);
|
|
197
200
|
if (!sig.ok)
|
|
198
|
-
return fail(401, "KYA_BAD_SIGNATURE", "signature does not match the credential's agent key over the
|
|
201
|
+
return fail(401, "KYA_BAD_SIGNATURE", "signature does not match the credential's agent key over the signed text");
|
|
199
202
|
if (v.status !== "active")
|
|
200
203
|
return fail(403, "KYA_CREDENTIAL_NOT_ACTIVE", `credential is ${v.status}${v.status_reason ? `: ${v.status_reason}` : ""}`);
|
|
201
204
|
const mandate = v.credential?.mandate ?? {};
|
|
@@ -213,9 +216,22 @@ export function createKyaAuth(opts = {}) {
|
|
|
213
216
|
};
|
|
214
217
|
return { ok: true, kya: ctx };
|
|
215
218
|
}
|
|
219
|
+
/** Low level: verify a signature by a credential's agent key over a text you built yourself (used for signed responses). */
|
|
220
|
+
async function verifySigned(p) {
|
|
221
|
+
let v;
|
|
222
|
+
try {
|
|
223
|
+
v = await verification(p.credentialId);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
const status = e.status;
|
|
227
|
+
return status === 404 ? fail(401, "KYA_CREDENTIAL_UNKNOWN", "unknown credential_id") : fail(502, "KYA_ISSUER_UNAVAILABLE", e.message);
|
|
228
|
+
}
|
|
229
|
+
return finish(p.credentialId, v, p.text, p.signature, p.body ?? "none");
|
|
230
|
+
}
|
|
216
231
|
const auth = {
|
|
217
232
|
client,
|
|
218
233
|
authenticateParts,
|
|
234
|
+
verifySigned,
|
|
219
235
|
async authenticate(request, o = {}) {
|
|
220
236
|
let body = o.body;
|
|
221
237
|
if (body === undefined && request.body && !["GET", "HEAD"].includes(request.method))
|
|
@@ -259,3 +275,97 @@ export function createKyaAuth(opts = {}) {
|
|
|
259
275
|
};
|
|
260
276
|
return auth;
|
|
261
277
|
}
|
|
278
|
+
// --- Mutual authentication (agent to agent) --------------------------------------------------
|
|
279
|
+
//
|
|
280
|
+
// When both sides are agents, the caller proves itself with the request headers above and the callee
|
|
281
|
+
// proves itself by signing the response, bound to the request it answers:
|
|
282
|
+
//
|
|
283
|
+
// KYA Brasil: response
|
|
284
|
+
// credential: <callee credential_id>
|
|
285
|
+
// in-reply-to: <sha256 hex of the request's KYA-Signature>
|
|
286
|
+
// created: <unix seconds>
|
|
287
|
+
// expires: <unix seconds>
|
|
288
|
+
// content-sha256: <hex of the response body | ->
|
|
289
|
+
//
|
|
290
|
+
// Response headers: the same KYA-* names plus KYA-In-Reply-To. The caller verifies with the callee's
|
|
291
|
+
// credential (agent key, active status) exactly as a server verifies a request.
|
|
292
|
+
export const KYA_REPLY_HEADER = "kya-in-reply-to";
|
|
293
|
+
export function responseText(p) {
|
|
294
|
+
return ["KYA Brasil: response", `credential: ${p.credentialId}`, `in-reply-to: ${p.inReplyTo}`, `created: ${p.created}`, `expires: ${p.expires}`, `content-sha256: ${p.contentSha256 ?? "-"}`].join("\n");
|
|
295
|
+
}
|
|
296
|
+
async function signText(key, text) {
|
|
297
|
+
if (key.kind === "web-bot-auth") {
|
|
298
|
+
const k = typeof key.privateKey === "string" ? await crypto.subtle.importKey("pkcs8", unb64u(key.privateKey.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")), { name: "Ed25519" }, false, ["sign"]) : key.privateKey;
|
|
299
|
+
return b64u(new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, k, enc.encode(text))));
|
|
300
|
+
}
|
|
301
|
+
return privateKeyToAccount(key.privateKey).signMessage({ message: text });
|
|
302
|
+
}
|
|
303
|
+
/** Callee side: headers that prove who answered, bound to the request signature it replies to. */
|
|
304
|
+
export async function signResponse(p) {
|
|
305
|
+
const created = Math.floor((p.now ?? new Date()).getTime() / 1000);
|
|
306
|
+
const expires = created + (p.ttlSeconds ?? 60);
|
|
307
|
+
const inReplyTo = await sha256Hex(p.requestSignature);
|
|
308
|
+
const contentSha256 = p.body !== undefined && p.body !== null && (typeof p.body !== "string" || p.body.length > 0) ? await sha256Hex(p.body) : null;
|
|
309
|
+
const signature = await signText(p.key, responseText({ credentialId: p.credentialId, inReplyTo, created, expires, contentSha256 }));
|
|
310
|
+
const headers = { [KYA_HEADERS.credential]: p.credentialId, [KYA_REPLY_HEADER]: inReplyTo, [KYA_HEADERS.created]: String(created), [KYA_HEADERS.expires]: String(expires), [KYA_HEADERS.signature]: signature };
|
|
311
|
+
if (contentSha256)
|
|
312
|
+
headers[KYA_HEADERS.content] = contentSha256;
|
|
313
|
+
return headers;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Caller side: verify that a response was signed by an active KYA credential and answers *this* request.
|
|
317
|
+
* `requestSignature` is the KYA-Signature header the caller sent.
|
|
318
|
+
*/
|
|
319
|
+
export async function verifyResponse(auth, response, requestSignature, opts = {}) {
|
|
320
|
+
const h = response.headers;
|
|
321
|
+
const credentialId = h.get(KYA_HEADERS.credential);
|
|
322
|
+
const inReplyTo = h.get(KYA_REPLY_HEADER);
|
|
323
|
+
const created = Number(h.get(KYA_HEADERS.created));
|
|
324
|
+
const expires = Number(h.get(KYA_HEADERS.expires));
|
|
325
|
+
const signature = h.get(KYA_HEADERS.signature);
|
|
326
|
+
const contentHeader = h.get(KYA_HEADERS.content);
|
|
327
|
+
const fail = (error, message) => ({ ok: false, error, message });
|
|
328
|
+
if (!credentialId || !inReplyTo || !signature || !Number.isFinite(created) || !Number.isFinite(expires))
|
|
329
|
+
return fail("KYA_RESPONSE_UNSIGNED", "the response carries no KYA signature");
|
|
330
|
+
if (inReplyTo !== (await sha256Hex(requestSignature)))
|
|
331
|
+
return fail("KYA_RESPONSE_MISMATCH", "the response answers a different request");
|
|
332
|
+
const now = Math.floor(Date.now() / 1000);
|
|
333
|
+
if (created > now + (opts.maxSkewSeconds ?? 300) || expires < now)
|
|
334
|
+
return fail("KYA_RESPONSE_EXPIRED", "response signature outside its validity window");
|
|
335
|
+
let body = opts.body;
|
|
336
|
+
if (body === undefined)
|
|
337
|
+
body = await response.clone().arrayBuffer();
|
|
338
|
+
let bodyState = "none";
|
|
339
|
+
if (body && (typeof body !== "string" ? body.byteLength > 0 : body.length > 0)) {
|
|
340
|
+
if (!contentHeader)
|
|
341
|
+
return fail("KYA_RESPONSE_BODY_UNSIGNED", "the response has a body but no content hash");
|
|
342
|
+
if ((await sha256Hex(body)) !== contentHeader.toLowerCase())
|
|
343
|
+
return fail("KYA_RESPONSE_BODY_MISMATCH", "response body does not match the signed hash");
|
|
344
|
+
bodyState = "verified";
|
|
345
|
+
}
|
|
346
|
+
// Same credential lookup, key selection and status rule as for requests, over the response text.
|
|
347
|
+
const probe = await auth.verifySigned({ credentialId, text: responseText({ credentialId, inReplyTo, created, expires, contentSha256: contentHeader }), signature, body: bodyState });
|
|
348
|
+
if (!probe.ok)
|
|
349
|
+
return fail(probe.error.replace("KYA_", "KYA_RESPONSE_"), probe.message);
|
|
350
|
+
return { ok: true, kya: probe.kya };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Caller side, all in one: a fetch that signs the request with your credential and, when the other side
|
|
354
|
+
* answers with KYA headers, verifies them. `callee` is null when the response was not signed.
|
|
355
|
+
*/
|
|
356
|
+
export function createKyaAgent(p) {
|
|
357
|
+
const auth = p.auth ?? createKyaAuth(p.authOptions);
|
|
358
|
+
const base = p.fetch ?? fetch;
|
|
359
|
+
return {
|
|
360
|
+
auth,
|
|
361
|
+
headers: (method, url, body) => signRequest({ credentialId: p.credentialId, key: p.key, method, url, body, ttlSeconds: p.ttlSeconds }),
|
|
362
|
+
async fetch(url, init = {}) {
|
|
363
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
364
|
+
const body = init.body ?? null;
|
|
365
|
+
const signed = await signRequest({ credentialId: p.credentialId, key: p.key, method, url, body, ttlSeconds: p.ttlSeconds });
|
|
366
|
+
const response = await base(url, { ...init, method, headers: { ...init.headers, ...signed }, body: body });
|
|
367
|
+
const callee = response.headers.get(KYA_HEADERS.signature) ? await verifyResponse(auth, response, signed[KYA_HEADERS.signature]) : null;
|
|
368
|
+
return { response, callee };
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-brasil/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "KYA Brasil for AI agents: verify that an agent acts for a Brazilian company (e-CNPJ-signed, KYB-checked credential), check an action against its mandate, or give your own agent a credential. Sign in with KYA middleware (Hono, Express, Workers) and request signing. x402 payments (USDC on Base) handled for you. Adapters for Vercel AI SDK, LangChain, OpenAI and Anthropic.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|