@kya-brasil/agent-tools 0.1.0 → 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/README.md +36 -2
- package/dist/auth.d.ts +130 -0
- package/dist/auth.js +261 -0
- package/dist/index.d.ts +74 -3
- package/dist/index.js +16 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,11 +29,15 @@ const verdict = await kya.allowed(credentialId, { // $0.01, signed
|
|
|
29
29
|
if (!verdict.allowed) reject(verdict.reason);
|
|
30
30
|
|
|
31
31
|
await verifyAttestation(verdict); // local Ed25519 check of the issuer's signature
|
|
32
|
+
|
|
33
|
+
const receipt = await kya.registerAct(credentialId, { // $0.02 once you accept it: signed receipt,
|
|
34
|
+
action: "order", amount: 4200, currency: "BRL", counterparty: "12345678000190", reference: "PO-1"
|
|
35
|
+
}); // daily ceilings enforced across counterparties
|
|
32
36
|
```
|
|
33
37
|
|
|
34
38
|
Also check that the key that signed the agent's request is `v.agent.id`; the credential_id alone proves nothing.
|
|
35
39
|
|
|
36
|
-
Agent frameworks (verifier tools `kya_verify`, `kya_check_action`, `kya_verify_paid`):
|
|
40
|
+
Agent frameworks (verifier tools `kya_verify`, `kya_check_action`, `kya_register_act`, `kya_verify_paid`):
|
|
37
41
|
|
|
38
42
|
```ts
|
|
39
43
|
import { toOpenAI, toVercelAI, toLangChain, toAnthropic } from "@kya-brasil/agent-tools";
|
|
@@ -42,6 +46,29 @@ toLangChain(kya, tool); // from "@langchain/core/tools"
|
|
|
42
46
|
toOpenAI(kya); toAnthropic(kya); // then kya.execute(name, args)
|
|
43
47
|
```
|
|
44
48
|
|
|
49
|
+
## Sign in with KYA: accept signed requests from agents
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createKyaAuth } from "@kya-brasil/agent-tools";
|
|
53
|
+
const auth = createKyaAuth(); // options: privateKey (for kya.allowed), cacheTtlMs, maxSkewSeconds
|
|
54
|
+
|
|
55
|
+
app.use("/api/*", auth.hono()); // Hono: c.get("kya")
|
|
56
|
+
app.use(auth.express()); // Express: req.kya
|
|
57
|
+
export default { fetch: auth.fetchHandler(async (req, kya) => Response.json({ hello: kya.principal.legal_name })) }; // Workers
|
|
58
|
+
const r = await auth.authenticate(request); // anywhere: { ok, kya } or { ok: false, status, error, message }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`kya` carries `credential_id`, `agent`, `principal`, `mandate`, `check(q)` (local mandate evaluation, no daily
|
|
62
|
+
ceilings) and `allowed(q)` (the issuer's signed verdict, $0.01). The agent signs with `signRequest(...)`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { signRequest } from "@kya-brasil/agent-tools";
|
|
66
|
+
const headers = await signRequest({ credentialId, key: { kind: "evm", privateKey: agentPk }, method: "POST", url, body });
|
|
67
|
+
await fetch(url, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Keys: `{ kind: "evm" | "erc8004", privateKey }` or `{ kind: "web-bot-auth", privateKey: CryptoKey | pkcs8 }`.
|
|
71
|
+
|
|
45
72
|
## Company: give your own agent a credential
|
|
46
73
|
|
|
47
74
|
```ts
|
|
@@ -52,7 +79,13 @@ const kya = createKyaClient({ privateKey: payerPk });
|
|
|
52
79
|
const reg = await kya.register({ // $0.50
|
|
53
80
|
agent: { kind: "evm", id: agentAddress },
|
|
54
81
|
cnpj: "11.222.333/0001-81",
|
|
55
|
-
mandate: {
|
|
82
|
+
mandate: {
|
|
83
|
+
actions: ["quote", "order", "pay"],
|
|
84
|
+
max_per_operation: { amount: 5000, currency: "BRL" },
|
|
85
|
+
max_per_day: { amount: 20000, currency: "BRL" },
|
|
86
|
+
currencies: ["BRL"],
|
|
87
|
+
counterparties: ["12345678000190", "*.beta.com.br"]
|
|
88
|
+
}
|
|
56
89
|
});
|
|
57
90
|
|
|
58
91
|
const agent_signature = await signChallengeWithEvmKey(agentPk, reg.challenge.text);
|
|
@@ -78,6 +111,7 @@ Renew every 90 days (`kya.renew` then `kya.renewProve`), revoke at once (`kya.re
|
|
|
78
111
|
|---|---|
|
|
79
112
|
| verify, verifyByKey, prove, renewProve, revoke, revocations | free |
|
|
80
113
|
| verified (signed receipt, no limit), allowed (signed verdict) | 0.01 |
|
|
114
|
+
| registerAct (signed receipt of what the agent did) | 0.02 |
|
|
81
115
|
| renew | 0.10 |
|
|
82
116
|
| register | 0.50 |
|
|
83
117
|
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type ActionQuery, type AgentKey, type CredentialStatus, type KyaClient, type Mandate, type Verdict, type Verification, type KyaOptions } from "./index";
|
|
2
|
+
export declare const KYA_HEADERS: {
|
|
3
|
+
readonly credential: "kya-credential";
|
|
4
|
+
readonly created: "kya-created";
|
|
5
|
+
readonly expires: "kya-expires";
|
|
6
|
+
readonly signature: "kya-signature";
|
|
7
|
+
readonly content: "kya-content-sha256";
|
|
8
|
+
};
|
|
9
|
+
export declare function sha256Hex(data: string | ArrayBuffer | Uint8Array): Promise<string>;
|
|
10
|
+
/** Canonical URL for the signature: origin (lowercase host) + path + query, no fragment. */
|
|
11
|
+
export declare function canonicalUrl(url: string): string;
|
|
12
|
+
export declare function requestText(p: {
|
|
13
|
+
credentialId: string;
|
|
14
|
+
method: string;
|
|
15
|
+
url: string;
|
|
16
|
+
created: number;
|
|
17
|
+
expires: number;
|
|
18
|
+
contentSha256?: string | null;
|
|
19
|
+
}): string;
|
|
20
|
+
export type SigningKey = {
|
|
21
|
+
kind: "evm" | "erc8004";
|
|
22
|
+
privateKey: `0x${string}`;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "web-bot-auth";
|
|
25
|
+
privateKey: CryptoKey | string;
|
|
26
|
+
};
|
|
27
|
+
/** Signs a request with the agent key and returns the KYA headers to send with it. */
|
|
28
|
+
export declare function signRequest(p: {
|
|
29
|
+
credentialId: string;
|
|
30
|
+
key: SigningKey;
|
|
31
|
+
method: string;
|
|
32
|
+
url: string;
|
|
33
|
+
body?: string | ArrayBuffer | Uint8Array | null;
|
|
34
|
+
ttlSeconds?: number;
|
|
35
|
+
now?: Date;
|
|
36
|
+
}): Promise<Record<string, string>>;
|
|
37
|
+
export interface KyaAuthOptions extends KyaOptions {
|
|
38
|
+
/** How long a fetched credential is reused before asking the issuer again (ms). Default 60 000. */
|
|
39
|
+
cacheTtlMs?: number;
|
|
40
|
+
/** Max clock difference accepted for `created` (seconds). Default 300. */
|
|
41
|
+
maxSkewSeconds?: number;
|
|
42
|
+
/** Max signature lifetime accepted (seconds), whatever `expires` says. Default 600. */
|
|
43
|
+
maxTtlSeconds?: number;
|
|
44
|
+
/** Verify the body hash when the request has a body. Default true. */
|
|
45
|
+
requireBodyHash?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface KyaContext {
|
|
48
|
+
credential_id: string;
|
|
49
|
+
agent: AgentKey;
|
|
50
|
+
principal: Verification["principal"];
|
|
51
|
+
status: CredentialStatus;
|
|
52
|
+
mandate: Mandate;
|
|
53
|
+
credential: Verification["credential"];
|
|
54
|
+
/** Which key verified the request signature. */
|
|
55
|
+
signer: string;
|
|
56
|
+
/** Whether the body hash was checked ("verified"), absent from the signature ("none") or not checkable ("skipped"). */
|
|
57
|
+
body: "verified" | "none" | "skipped";
|
|
58
|
+
/** Local evaluation of the mandate (no daily ceilings; those need the issuer). */
|
|
59
|
+
check(q: ActionQuery): LocalVerdict;
|
|
60
|
+
/** Signed verdict from the issuer ($0.01, needs privateKey or internalKey in the options). */
|
|
61
|
+
allowed(q: ActionQuery): Promise<Verdict>;
|
|
62
|
+
}
|
|
63
|
+
export type KyaAuthResult = {
|
|
64
|
+
ok: true;
|
|
65
|
+
kya: KyaContext;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
status: 401 | 403 | 502;
|
|
69
|
+
error: string;
|
|
70
|
+
message: string;
|
|
71
|
+
};
|
|
72
|
+
export interface LocalVerdict {
|
|
73
|
+
allowed: boolean;
|
|
74
|
+
checks: {
|
|
75
|
+
rule: string;
|
|
76
|
+
ok: boolean;
|
|
77
|
+
detail: string;
|
|
78
|
+
}[];
|
|
79
|
+
not_evaluated: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Local evaluation of the reserved mandate fields: actions, max_per_operation, currencies, counterparties
|
|
83
|
+
* (with `*.` wildcards), valid_from / valid_until. `max_per_day` is listed as not evaluated: only the issuer
|
|
84
|
+
* knows what was registered today (use ctx.allowed or registerAct).
|
|
85
|
+
*/
|
|
86
|
+
export declare function checkMandate(mandate: Mandate, q: ActionQuery, now?: Date): LocalVerdict;
|
|
87
|
+
export interface KyaAuth {
|
|
88
|
+
client: KyaClient;
|
|
89
|
+
/** Authenticate any Web-standard Request. Never throws; read `ok`. */
|
|
90
|
+
authenticate(request: Request, opts?: {
|
|
91
|
+
body?: string | ArrayBuffer | Uint8Array | null;
|
|
92
|
+
}): Promise<KyaAuthResult>;
|
|
93
|
+
/** Same, from the parts (for frameworks that already parsed the request). */
|
|
94
|
+
authenticateParts(p: {
|
|
95
|
+
method: string;
|
|
96
|
+
url: string;
|
|
97
|
+
headers: Headers | Record<string, string | string[] | undefined>;
|
|
98
|
+
body?: string | ArrayBuffer | Uint8Array | null;
|
|
99
|
+
}): Promise<KyaAuthResult>;
|
|
100
|
+
/** Hono middleware: sets c.set("kya", ctx) or answers 401/403 JSON. */
|
|
101
|
+
hono(): (c: HonoLikeContext, next: () => Promise<void>) => Promise<Response | void>;
|
|
102
|
+
/** Express middleware: sets req.kya or answers 401/403 JSON. Verifies the body when req.body is a string or Buffer. */
|
|
103
|
+
express(): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: (err?: unknown) => void) => void;
|
|
104
|
+
/** Wrap a fetch handler (Workers, Deno, Bun): handler(request, kya). */
|
|
105
|
+
fetchHandler(handler: (request: Request, kya: KyaContext) => Promise<Response> | Response): (request: Request) => Promise<Response>;
|
|
106
|
+
/** Forget cached credentials (tests, or after a revocation you learned about). */
|
|
107
|
+
clearCache(): void;
|
|
108
|
+
}
|
|
109
|
+
export interface HonoLikeContext {
|
|
110
|
+
req: {
|
|
111
|
+
raw: Request;
|
|
112
|
+
};
|
|
113
|
+
set: (k: string, v: unknown) => void;
|
|
114
|
+
json: (body: unknown, status?: number) => Response;
|
|
115
|
+
}
|
|
116
|
+
export interface ExpressLikeRequest {
|
|
117
|
+
method: string;
|
|
118
|
+
protocol?: string;
|
|
119
|
+
originalUrl?: string;
|
|
120
|
+
url?: string;
|
|
121
|
+
headers: Record<string, string | string[] | undefined>;
|
|
122
|
+
body?: unknown;
|
|
123
|
+
get?: (name: string) => string | undefined;
|
|
124
|
+
kya?: KyaContext;
|
|
125
|
+
}
|
|
126
|
+
export interface ExpressLikeResponse {
|
|
127
|
+
status: (code: number) => ExpressLikeResponse;
|
|
128
|
+
json: (body: unknown) => unknown;
|
|
129
|
+
}
|
|
130
|
+
export declare function createKyaAuth(opts?: KyaAuthOptions): KyaAuth;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Sign in with KYA": authenticate an incoming request from an agent in one step.
|
|
3
|
+
*
|
|
4
|
+
* The agent signs each request with its key and sends four headers; your server verifies the signature,
|
|
5
|
+
* fetches (and caches) the credential, checks that it is active, and hands you who the agent is, which
|
|
6
|
+
* company answers for it, and its mandate. Works with any Web-standard Request (Workers, Hono, Next,
|
|
7
|
+
* Bun, Deno) and with Express through a small adapter.
|
|
8
|
+
*
|
|
9
|
+
* Server:
|
|
10
|
+
* const auth = createKyaAuth();
|
|
11
|
+
* app.use("/api/*", auth.hono()); // c.get("kya") → { agent, principal, mandate, ... }
|
|
12
|
+
* app.use(auth.express()); // req.kya
|
|
13
|
+
* const r = await auth.authenticate(request); // anywhere: { ok, ... }
|
|
14
|
+
*
|
|
15
|
+
* Agent:
|
|
16
|
+
* const headers = await signRequest({ credentialId, key: { kind: "evm", privateKey }, method: "POST", url, body });
|
|
17
|
+
* await fetch(url, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body });
|
|
18
|
+
*
|
|
19
|
+
* Request text (what the agent signs; the profile is in docs/SPEC.md section 15):
|
|
20
|
+
* KYA Brasil: request
|
|
21
|
+
* credential: <credential_id>
|
|
22
|
+
* method: <METHOD>
|
|
23
|
+
* url: <origin + path + query, lowercase host>
|
|
24
|
+
* created: <unix seconds>
|
|
25
|
+
* expires: <unix seconds>
|
|
26
|
+
* content-sha256: <hex of the body | ->
|
|
27
|
+
*/
|
|
28
|
+
import { verifyMessage } from "viem";
|
|
29
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
30
|
+
import { createKyaClient } from "./index";
|
|
31
|
+
export const KYA_HEADERS = { credential: "kya-credential", created: "kya-created", expires: "kya-expires", signature: "kya-signature", content: "kya-content-sha256" };
|
|
32
|
+
const enc = new TextEncoder();
|
|
33
|
+
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
34
|
+
const b64u = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
35
|
+
const unb64u = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
|
|
36
|
+
export async function sha256Hex(data) {
|
|
37
|
+
const bytes = typeof data === "string" ? enc.encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
38
|
+
return hex(await crypto.subtle.digest("SHA-256", bytes));
|
|
39
|
+
}
|
|
40
|
+
/** Canonical URL for the signature: origin (lowercase host) + path + query, no fragment. */
|
|
41
|
+
export function canonicalUrl(url) {
|
|
42
|
+
const u = new URL(url);
|
|
43
|
+
return `${u.protocol}//${u.host.toLowerCase()}${u.pathname}${u.search}`;
|
|
44
|
+
}
|
|
45
|
+
export function requestText(p) {
|
|
46
|
+
return [
|
|
47
|
+
"KYA Brasil: request",
|
|
48
|
+
`credential: ${p.credentialId}`,
|
|
49
|
+
`method: ${p.method.toUpperCase()}`,
|
|
50
|
+
`url: ${canonicalUrl(p.url)}`,
|
|
51
|
+
`created: ${p.created}`,
|
|
52
|
+
`expires: ${p.expires}`,
|
|
53
|
+
`content-sha256: ${p.contentSha256 ?? "-"}`
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
/** Signs a request with the agent key and returns the KYA headers to send with it. */
|
|
57
|
+
export async function signRequest(p) {
|
|
58
|
+
const created = Math.floor((p.now ?? new Date()).getTime() / 1000);
|
|
59
|
+
const expires = created + (p.ttlSeconds ?? 60);
|
|
60
|
+
const contentSha256 = p.body !== undefined && p.body !== null && (typeof p.body !== "string" || p.body.length > 0) ? await sha256Hex(p.body) : null;
|
|
61
|
+
const text = requestText({ credentialId: p.credentialId, method: p.method, url: p.url, created, expires, contentSha256 });
|
|
62
|
+
let signature;
|
|
63
|
+
if (p.key.kind === "web-bot-auth") {
|
|
64
|
+
const key = typeof p.key.privateKey === "string" ? await crypto.subtle.importKey("pkcs8", unb64u(p.key.privateKey.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")), { name: "Ed25519" }, false, ["sign"]) : p.key.privateKey;
|
|
65
|
+
signature = b64u(new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, enc.encode(text))));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
signature = await privateKeyToAccount(p.key.privateKey).signMessage({ message: text });
|
|
69
|
+
}
|
|
70
|
+
const headers = { [KYA_HEADERS.credential]: p.credentialId, [KYA_HEADERS.created]: String(created), [KYA_HEADERS.expires]: String(expires), [KYA_HEADERS.signature]: signature };
|
|
71
|
+
if (contentSha256)
|
|
72
|
+
headers[KYA_HEADERS.content] = contentSha256;
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
const RESERVED = ["actions", "max_per_operation", "max_per_day", "currencies", "counterparties", "valid_from", "valid_until"];
|
|
76
|
+
function normalizeCounterparty(c) {
|
|
77
|
+
const t = c.trim();
|
|
78
|
+
const digits = t.replace(/\D/g, "");
|
|
79
|
+
if (digits.length === 14 && /^[\d./-]+$/.test(t))
|
|
80
|
+
return digits;
|
|
81
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(t))
|
|
82
|
+
return t.toLowerCase();
|
|
83
|
+
return t.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Local evaluation of the reserved mandate fields: actions, max_per_operation, currencies, counterparties
|
|
87
|
+
* (with `*.` wildcards), valid_from / valid_until. `max_per_day` is listed as not evaluated: only the issuer
|
|
88
|
+
* knows what was registered today (use ctx.allowed or registerAct).
|
|
89
|
+
*/
|
|
90
|
+
export function checkMandate(mandate, q, now = new Date()) {
|
|
91
|
+
const checks = [];
|
|
92
|
+
const push = (rule, ok, detail) => checks.push({ rule, ok, detail });
|
|
93
|
+
if (mandate.valid_from)
|
|
94
|
+
push("valid_from", now >= new Date(mandate.valid_from), `mandate starts ${mandate.valid_from}`);
|
|
95
|
+
if (mandate.valid_until)
|
|
96
|
+
push("valid_until", now <= new Date(mandate.valid_until), `mandate ends ${mandate.valid_until}`);
|
|
97
|
+
if (mandate.actions && q.action)
|
|
98
|
+
push("actions", mandate.actions.includes(q.action), `action "${q.action}" ${mandate.actions.includes(q.action) ? "is" : "is not"} in [${mandate.actions.join(", ")}]`);
|
|
99
|
+
if (mandate.currencies && q.currency) {
|
|
100
|
+
const c = q.currency.toUpperCase();
|
|
101
|
+
push("currencies", mandate.currencies.includes(c), `currency ${c} ${mandate.currencies.includes(c) ? "allowed" : "not in"} [${mandate.currencies.join(", ")}]`);
|
|
102
|
+
}
|
|
103
|
+
if (mandate.max_per_operation && q.amount !== undefined) {
|
|
104
|
+
const same = !q.currency || q.currency.toUpperCase() === mandate.max_per_operation.currency;
|
|
105
|
+
push("max_per_operation", same && q.amount <= mandate.max_per_operation.amount, same ? `${q.amount} vs ceiling ${mandate.max_per_operation.amount} ${mandate.max_per_operation.currency}` : `ceiling is in ${mandate.max_per_operation.currency}, operation in ${q.currency}`);
|
|
106
|
+
}
|
|
107
|
+
if (mandate.counterparties && mandate.counterparties.length && q.counterparty) {
|
|
108
|
+
const c = normalizeCounterparty(q.counterparty);
|
|
109
|
+
const ok = mandate.counterparties.some((m) => {
|
|
110
|
+
const n = normalizeCounterparty(m);
|
|
111
|
+
return n === c || (n.startsWith("*.") && (c === n.slice(2) || c.endsWith(n.slice(1))));
|
|
112
|
+
});
|
|
113
|
+
push("counterparties", ok, `${c} ${ok ? "is" : "is not"} an allowed counterparty`);
|
|
114
|
+
}
|
|
115
|
+
const not_evaluated = Object.keys(mandate).filter((k) => !RESERVED.includes(k));
|
|
116
|
+
if (mandate.max_per_day)
|
|
117
|
+
not_evaluated.push("max_per_day (only the issuer knows today's registered total)");
|
|
118
|
+
return { allowed: checks.every((c) => c.ok), checks, not_evaluated };
|
|
119
|
+
}
|
|
120
|
+
const headerOf = (headers, name) => {
|
|
121
|
+
if (headers instanceof Headers)
|
|
122
|
+
return headers.get(name);
|
|
123
|
+
const v = headers[name] ?? headers[name.toLowerCase()];
|
|
124
|
+
return Array.isArray(v) ? v[0] ?? null : v ?? null;
|
|
125
|
+
};
|
|
126
|
+
export function createKyaAuth(opts = {}) {
|
|
127
|
+
const client = createKyaClient(opts);
|
|
128
|
+
const cacheTtl = opts.cacheTtlMs ?? 60_000;
|
|
129
|
+
const maxSkew = opts.maxSkewSeconds ?? 300;
|
|
130
|
+
const maxTtl = opts.maxTtlSeconds ?? 600;
|
|
131
|
+
const requireBodyHash = opts.requireBodyHash ?? true;
|
|
132
|
+
const cache = new Map();
|
|
133
|
+
async function verification(id) {
|
|
134
|
+
const hit = cache.get(id);
|
|
135
|
+
if (hit && Date.now() - hit.at < cacheTtl)
|
|
136
|
+
return hit.v;
|
|
137
|
+
const v = await client.verify(id);
|
|
138
|
+
cache.set(id, { at: Date.now(), v });
|
|
139
|
+
return v;
|
|
140
|
+
}
|
|
141
|
+
async function verifySignature(agent, text, signature) {
|
|
142
|
+
try {
|
|
143
|
+
if (agent.kind === "web-bot-auth") {
|
|
144
|
+
const key = await crypto.subtle.importKey("raw", unb64u(agent.id), { name: "Ed25519" }, false, ["verify"]);
|
|
145
|
+
const sig = unb64u(signature);
|
|
146
|
+
return { ok: sig.length === 64 && (await crypto.subtle.verify({ name: "Ed25519" }, key, sig, enc.encode(text))), signer: agent.id };
|
|
147
|
+
}
|
|
148
|
+
const address = agent.kind === "evm" ? agent.id : agent.wallet;
|
|
149
|
+
if (!address || !/^0x[0-9a-fA-F]{130}$/.test(signature))
|
|
150
|
+
return { ok: false, signer: address ?? "" };
|
|
151
|
+
return { ok: await verifyMessage({ address: address, message: text, signature: signature }), signer: address };
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { ok: false, signer: "" };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const fail = (status, error, message) => ({ ok: false, status, error, message });
|
|
158
|
+
async function authenticateParts(p) {
|
|
159
|
+
const credentialId = headerOf(p.headers, KYA_HEADERS.credential);
|
|
160
|
+
const created = Number(headerOf(p.headers, KYA_HEADERS.created));
|
|
161
|
+
const expires = Number(headerOf(p.headers, KYA_HEADERS.expires));
|
|
162
|
+
const signature = headerOf(p.headers, KYA_HEADERS.signature);
|
|
163
|
+
const contentHeader = headerOf(p.headers, KYA_HEADERS.content);
|
|
164
|
+
if (!credentialId || !signature || !Number.isFinite(created) || !Number.isFinite(expires))
|
|
165
|
+
return fail(401, "KYA_HEADERS_MISSING", `send ${KYA_HEADERS.credential}, ${KYA_HEADERS.created}, ${KYA_HEADERS.expires} and ${KYA_HEADERS.signature}`);
|
|
166
|
+
const now = Math.floor(Date.now() / 1000);
|
|
167
|
+
if (created > now + maxSkew)
|
|
168
|
+
return fail(401, "KYA_SIGNATURE_NOT_YET_VALID", "created is in the future");
|
|
169
|
+
if (expires < now)
|
|
170
|
+
return fail(401, "KYA_SIGNATURE_EXPIRED", "expires is in the past");
|
|
171
|
+
if (expires - created > maxTtl || expires < created)
|
|
172
|
+
return fail(401, "KYA_SIGNATURE_TTL", `signature lifetime must be between 0 and ${maxTtl}s`);
|
|
173
|
+
let bodyState = "none";
|
|
174
|
+
if (p.body !== undefined && p.body !== null && (typeof p.body !== "string" || p.body.length > 0)) {
|
|
175
|
+
if (!contentHeader) {
|
|
176
|
+
if (requireBodyHash)
|
|
177
|
+
return fail(401, "KYA_BODY_UNSIGNED", `the request has a body but no ${KYA_HEADERS.content} header`);
|
|
178
|
+
bodyState = "skipped";
|
|
179
|
+
}
|
|
180
|
+
else if ((await sha256Hex(p.body)) !== contentHeader.toLowerCase())
|
|
181
|
+
return fail(401, "KYA_BODY_MISMATCH", "body hash does not match the signed hash");
|
|
182
|
+
else
|
|
183
|
+
bodyState = "verified";
|
|
184
|
+
}
|
|
185
|
+
else if (contentHeader)
|
|
186
|
+
bodyState = "skipped";
|
|
187
|
+
let v;
|
|
188
|
+
try {
|
|
189
|
+
v = await verification(credentialId);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
const status = e.status;
|
|
193
|
+
return status === 404 ? fail(401, "KYA_CREDENTIAL_UNKNOWN", "unknown credential_id") : fail(502, "KYA_ISSUER_UNAVAILABLE", e.message);
|
|
194
|
+
}
|
|
195
|
+
const text = requestText({ credentialId, method: p.method, url: p.url, created, expires, contentSha256: contentHeader });
|
|
196
|
+
const sig = await verifySignature(v.agent, text, signature);
|
|
197
|
+
if (!sig.ok)
|
|
198
|
+
return fail(401, "KYA_BAD_SIGNATURE", "signature does not match the credential's agent key over the request text");
|
|
199
|
+
if (v.status !== "active")
|
|
200
|
+
return fail(403, "KYA_CREDENTIAL_NOT_ACTIVE", `credential is ${v.status}${v.status_reason ? `: ${v.status_reason}` : ""}`);
|
|
201
|
+
const mandate = v.credential?.mandate ?? {};
|
|
202
|
+
const ctx = {
|
|
203
|
+
credential_id: credentialId,
|
|
204
|
+
agent: v.agent,
|
|
205
|
+
principal: v.principal,
|
|
206
|
+
status: v.status,
|
|
207
|
+
mandate,
|
|
208
|
+
credential: v.credential,
|
|
209
|
+
signer: sig.signer,
|
|
210
|
+
body: bodyState,
|
|
211
|
+
check: (q) => checkMandate(mandate, q),
|
|
212
|
+
allowed: (q) => client.allowed(credentialId, q)
|
|
213
|
+
};
|
|
214
|
+
return { ok: true, kya: ctx };
|
|
215
|
+
}
|
|
216
|
+
const auth = {
|
|
217
|
+
client,
|
|
218
|
+
authenticateParts,
|
|
219
|
+
async authenticate(request, o = {}) {
|
|
220
|
+
let body = o.body;
|
|
221
|
+
if (body === undefined && request.body && !["GET", "HEAD"].includes(request.method))
|
|
222
|
+
body = await request.clone().arrayBuffer();
|
|
223
|
+
return authenticateParts({ method: request.method, url: request.url, headers: request.headers, body });
|
|
224
|
+
},
|
|
225
|
+
hono() {
|
|
226
|
+
return async (c, next) => {
|
|
227
|
+
const r = await auth.authenticate(c.req.raw);
|
|
228
|
+
if (!r.ok)
|
|
229
|
+
return c.json({ error: r.error, message: r.message }, r.status);
|
|
230
|
+
c.set("kya", r.kya);
|
|
231
|
+
await next();
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
express() {
|
|
235
|
+
return (req, res, next) => {
|
|
236
|
+
const host = req.get?.("host") ?? (headerOf(req.headers, "host") || "localhost");
|
|
237
|
+
const proto = headerOf(req.headers, "x-forwarded-proto") || req.protocol || "http";
|
|
238
|
+
const url = `${proto}://${host}${req.originalUrl ?? req.url ?? "/"}`;
|
|
239
|
+
const raw = typeof req.body === "string" || req.body instanceof Uint8Array ? req.body : req.body && typeof req.body === "object" ? JSON.stringify(req.body) : undefined;
|
|
240
|
+
authenticateParts({ method: req.method, url, headers: req.headers, body: raw })
|
|
241
|
+
.then((r) => {
|
|
242
|
+
if (!r.ok)
|
|
243
|
+
return res.status(r.status).json({ error: r.error, message: r.message });
|
|
244
|
+
req.kya = r.kya;
|
|
245
|
+
next();
|
|
246
|
+
})
|
|
247
|
+
.catch(next);
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
fetchHandler(handler) {
|
|
251
|
+
return async (request) => {
|
|
252
|
+
const r = await auth.authenticate(request);
|
|
253
|
+
if (!r.ok)
|
|
254
|
+
return new Response(JSON.stringify({ error: r.error, message: r.message }), { status: r.status, headers: { "content-type": "application/json" } });
|
|
255
|
+
return handler(request, r.kya);
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
clearCache: () => cache.clear()
|
|
259
|
+
};
|
|
260
|
+
return auth;
|
|
261
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,15 +8,23 @@ export interface KyaOptions {
|
|
|
8
8
|
internalKey?: string;
|
|
9
9
|
fetch?: typeof fetch;
|
|
10
10
|
}
|
|
11
|
+
/** erc8004: id is eip155:<chainId>:<identityRegistry>:<agentId>; the on-chain agentWallet signs (credential.agent.wallet). */
|
|
11
12
|
export type AgentKey = {
|
|
12
|
-
kind: "evm" | "web-bot-auth";
|
|
13
|
+
kind: "evm" | "web-bot-auth" | "erc8004";
|
|
13
14
|
id: string;
|
|
15
|
+
wallet?: `0x${string}`;
|
|
14
16
|
};
|
|
15
17
|
export type CredentialStatus = "pending" | "active" | "suspended" | "revoked" | "expired";
|
|
16
18
|
export interface Mandate {
|
|
17
19
|
actions?: string[];
|
|
18
|
-
max_per_operation?:
|
|
19
|
-
|
|
20
|
+
max_per_operation?: {
|
|
21
|
+
amount: number;
|
|
22
|
+
currency: string;
|
|
23
|
+
};
|
|
24
|
+
max_per_day?: {
|
|
25
|
+
amount: number;
|
|
26
|
+
currency: string;
|
|
27
|
+
};
|
|
20
28
|
currencies?: string[];
|
|
21
29
|
counterparties?: string[];
|
|
22
30
|
valid_from?: string;
|
|
@@ -123,6 +131,45 @@ export interface Proof {
|
|
|
123
131
|
agent_signature: string;
|
|
124
132
|
ecnpj_signature: string;
|
|
125
133
|
}
|
|
134
|
+
export interface ActInput {
|
|
135
|
+
action: string;
|
|
136
|
+
amount?: number;
|
|
137
|
+
currency?: string;
|
|
138
|
+
counterparty?: string;
|
|
139
|
+
reference?: string;
|
|
140
|
+
content_sha256?: string;
|
|
141
|
+
registered_by?: "counterparty" | "agent";
|
|
142
|
+
registrant?: string;
|
|
143
|
+
agent_signature?: string;
|
|
144
|
+
}
|
|
145
|
+
export interface ActReceipt {
|
|
146
|
+
act_id: string;
|
|
147
|
+
credential_id: string;
|
|
148
|
+
status: CredentialStatus;
|
|
149
|
+
allowed: boolean;
|
|
150
|
+
reason: string;
|
|
151
|
+
act: ActInput & {
|
|
152
|
+
agent_signed: boolean;
|
|
153
|
+
};
|
|
154
|
+
checks: {
|
|
155
|
+
rule: string;
|
|
156
|
+
ok: boolean;
|
|
157
|
+
detail?: string;
|
|
158
|
+
}[];
|
|
159
|
+
not_evaluated: string[];
|
|
160
|
+
spent_today_registered_before: {
|
|
161
|
+
currency: string;
|
|
162
|
+
amount: number;
|
|
163
|
+
} | null;
|
|
164
|
+
spent_today_registered_after: {
|
|
165
|
+
currency: string;
|
|
166
|
+
amount: number;
|
|
167
|
+
} | null;
|
|
168
|
+
registered_at: string;
|
|
169
|
+
receipt_url: string;
|
|
170
|
+
attestation?: Attestation;
|
|
171
|
+
[k: string]: unknown;
|
|
172
|
+
}
|
|
126
173
|
export declare class KyaError extends Error {
|
|
127
174
|
readonly code: string;
|
|
128
175
|
readonly status: number;
|
|
@@ -176,6 +223,27 @@ export interface KyaClient {
|
|
|
176
223
|
credential_id: string;
|
|
177
224
|
status: "revoked";
|
|
178
225
|
}>;
|
|
226
|
+
/** $0.02: register what the agent did (you accepted it); mandate re-checked with today's registered acts; signed receipt. */
|
|
227
|
+
registerAct(credentialId: string, act: ActInput): Promise<ActReceipt>;
|
|
228
|
+
/** Free: a registered act and its receipt, by act_id. */
|
|
229
|
+
act(actId: string): Promise<ActReceipt>;
|
|
230
|
+
/** Free: the exact text the agent signs for an act (optional agent_signature). */
|
|
231
|
+
actText(credentialId: string, act: ActInput): Promise<{
|
|
232
|
+
credential_id: string;
|
|
233
|
+
agent: AgentKey;
|
|
234
|
+
text: string;
|
|
235
|
+
}>;
|
|
236
|
+
/** Free, company only: the trail of an agent's acts, request signed with the e-CNPJ over actsExportText(id). */
|
|
237
|
+
exportActs(credentialId: string, input: {
|
|
238
|
+
signed_text: string;
|
|
239
|
+
ecnpj_signature: string;
|
|
240
|
+
since?: string;
|
|
241
|
+
}): Promise<{
|
|
242
|
+
credential_id: string;
|
|
243
|
+
since: string;
|
|
244
|
+
count: number;
|
|
245
|
+
acts: Record<string, unknown>[];
|
|
246
|
+
}>;
|
|
179
247
|
/** Free: credentials revoked since a date (default 30 days). */
|
|
180
248
|
revocations(since?: string): Promise<{
|
|
181
249
|
since: string;
|
|
@@ -248,6 +316,8 @@ export interface ECnpjMaterial {
|
|
|
248
316
|
* Works with A1 certificates (file). A3 (token or smart card) must sign through the vendor's software.
|
|
249
317
|
*/
|
|
250
318
|
export declare function signWithECnpj(material: ECnpjMaterial, text: string): Promise<string>;
|
|
319
|
+
/** The text the company signs to read its agent's trail of acts. */
|
|
320
|
+
export declare function actsExportText(credentialId: string, nonce?: string): string;
|
|
251
321
|
/** The text the company signs to revoke a credential. */
|
|
252
322
|
export declare function revocationText(credentialId: string, nonce?: string): string;
|
|
253
323
|
export interface VerifyResult {
|
|
@@ -267,3 +337,4 @@ export declare function verifyAttestation(doc: Record<string, unknown>, opts?: {
|
|
|
267
337
|
publicJwk?: JsonWebKey;
|
|
268
338
|
fetch?: typeof fetch;
|
|
269
339
|
}): Promise<VerifyResult>;
|
|
340
|
+
export * from "./auth";
|
package/dist/index.js
CHANGED
|
@@ -80,6 +80,10 @@ export function createKyaClient(opts = {}) {
|
|
|
80
80
|
renewProve: (id, proof) => call("POST", `/v1/agents/${encodeURIComponent(id)}/renew/prove`, proof),
|
|
81
81
|
revoke: (id, input) => call("POST", `/v1/agents/${encodeURIComponent(id)}/revoke`, input),
|
|
82
82
|
revocations: (since) => call("GET", `/v1/revocations${q({ since })}`),
|
|
83
|
+
registerAct: (id, act) => call("POST", `/v1/agents/${encodeURIComponent(id)}/acts`, act, true, 0.02),
|
|
84
|
+
act: (actId) => call("GET", `/v1/acts/${encodeURIComponent(actId)}`),
|
|
85
|
+
actText: (id, act) => call("POST", `/v1/agents/${encodeURIComponent(id)}/acts/text`, act),
|
|
86
|
+
exportActs: (id, input) => call("POST", `/v1/agents/${encodeURIComponent(id)}/acts/export`, input),
|
|
83
87
|
tools: [],
|
|
84
88
|
execute: async (name, args) => {
|
|
85
89
|
const t = client.tools.find((x) => x.name === name);
|
|
@@ -93,7 +97,7 @@ export function createKyaClient(opts = {}) {
|
|
|
93
97
|
{
|
|
94
98
|
name: "kya_verify", priceUsd: 0,
|
|
95
99
|
description: "Free. Is this AI agent credentialed by a Brazilian company? Returns the credential (company CNPJ and name, mandate, KYB snapshot), its live status (active, suspended, revoked, expired) and history. Give credential_id, or the agent key (kind + id) to look it up.",
|
|
96
|
-
parameters: { type: "object", properties: { credential_id: ID, kind: { type: "string", enum: ["evm", "web-bot-auth"] }, id: { type: "string", description: "0x address
|
|
100
|
+
parameters: { type: "object", properties: { credential_id: ID, kind: { type: "string", enum: ["evm", "web-bot-auth", "erc8004"] }, id: { type: "string", description: "0x address, base64url Ed25519 key, or eip155:<chainId>:<registry>:<agentId>" } }, additionalProperties: false },
|
|
97
101
|
execute: async (a) => (a.credential_id ? client.verify(String(a.credential_id)) : client.verifyByKey({ kind: a.kind, id: String(a.id) }))
|
|
98
102
|
},
|
|
99
103
|
{
|
|
@@ -102,6 +106,12 @@ export function createKyaClient(opts = {}) {
|
|
|
102
106
|
parameters: { type: "object", properties: { credential_id: ID, action: { type: "string" }, amount: { type: "number" }, currency: { type: "string" }, counterparty: { type: "string", description: "Your CNPJ, domain or wallet address" }, spent_today: { type: "number" } }, required: ["credential_id"], additionalProperties: false },
|
|
103
107
|
execute: async (a) => client.allowed(String(a.credential_id), { action: a.action, amount: a.amount, currency: a.currency, counterparty: a.counterparty, spent_today: a.spent_today })
|
|
104
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: "kya_register_act", priceUsd: 0.02,
|
|
111
|
+
description: "Register an action you accepted from an agent ($0.02): the issuer re-checks the mandate including everything registered today by anyone (so daily ceilings hold across counterparties) and returns a signed receipt, your proof of what the agent could and did do. Denied acts are recorded too.",
|
|
112
|
+
parameters: { type: "object", properties: { credential_id: ID, action: { type: "string" }, amount: { type: "number" }, currency: { type: "string" }, counterparty: { type: "string", description: "You: CNPJ, domain or wallet" }, reference: { type: "string" }, content_sha256: { type: "string" } }, required: ["credential_id", "action"], additionalProperties: false },
|
|
113
|
+
execute: async (a) => { const { credential_id, ...act } = a; return client.registerAct(String(credential_id), act); }
|
|
114
|
+
},
|
|
105
115
|
{
|
|
106
116
|
name: "kya_verify_paid", priceUsd: 0.01,
|
|
107
117
|
description: "Same as kya_verify without the daily limit, plus a signed receipt of the verification ($0.01).",
|
|
@@ -163,6 +173,10 @@ export async function signWithECnpj(material, text) {
|
|
|
163
173
|
s += String.fromCharCode(b);
|
|
164
174
|
return btoa(s);
|
|
165
175
|
}
|
|
176
|
+
/** The text the company signs to read its agent's trail of acts. */
|
|
177
|
+
export function actsExportText(credentialId, nonce = crypto.randomUUID()) {
|
|
178
|
+
return `KYA Brasil: acts ${credentialId}\n${nonce}`;
|
|
179
|
+
}
|
|
166
180
|
/** The text the company signs to revoke a credential. */
|
|
167
181
|
export function revocationText(credentialId, nonce = crypto.randomUUID()) {
|
|
168
182
|
return `KYA Brasil: revoke ${credentialId}\n${nonce}`;
|
|
@@ -204,3 +218,4 @@ export async function verifyAttestation(doc, opts = {}) {
|
|
|
204
218
|
const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, sig, new TextEncoder().encode(`${hex}\n${att.signed_at}`));
|
|
205
219
|
return ok ? { valid: true, key_id: att.key_id, signed_at: att.signed_at } : { valid: false, reason: "signature does not verify", key_id: att.key_id, signed_at: att.signed_at };
|
|
206
220
|
}
|
|
221
|
+
export * from "./auth";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-brasil/agent-tools",
|
|
3
|
-
"version": "0.
|
|
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. x402 payments (USDC on Base) handled for you. Adapters for Vercel AI SDK, LangChain, OpenAI and Anthropic.",
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"main": "./dist/index.js",
|