@kya-brasil/agent-tools 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,85 @@
1
- # Temporary Holding Version
1
+ # @kya-brasil/agent-tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Know-your-agent for Brazil, as code.** Verify that an AI agent really acts for a Brazilian company
4
+ (credential signed by the company's e-CNPJ, company cleared by [KYB Brasil](https://kyb-br-agents.marcelfons.workers.dev)),
5
+ check an action against the agent's mandate, or give your own agent a credential.
6
+
7
+ Paid per call with [x402](https://x402.org) (USDC on Base, no signup). Free verification works without a wallet
8
+ (50/day per IP). Errors are never charged. MCP endpoint for agent frameworks: `https://kya-br-agents.marcelfons.workers.dev/mcp`.
9
+
10
+ ```bash
11
+ npm i @kya-brasil/agent-tools
12
+ ```
13
+
14
+ Node 18+ (global `fetch`, WebCrypto). ESM.
15
+
16
+ ## Verifier: you receive an action from someone else's agent
17
+
18
+ ```ts
19
+ import { createKyaClient, verifyAttestation } from "@kya-brasil/agent-tools";
20
+
21
+ const kya = createKyaClient({ privateKey: process.env.KYA_PAYER_PK as `0x${string}` }); // wallet optional
22
+
23
+ const v = await kya.verify(credentialId); // free
24
+ if (v.status !== "active") reject();
25
+
26
+ const verdict = await kya.allowed(credentialId, { // $0.01, signed
27
+ action: "order", amount: 4200, currency: "BRL", counterparty: "12345678000190"
28
+ });
29
+ if (!verdict.allowed) reject(verdict.reason);
30
+
31
+ await verifyAttestation(verdict); // local Ed25519 check of the issuer's signature
32
+ ```
33
+
34
+ Also check that the key that signed the agent's request is `v.agent.id`; the credential_id alone proves nothing.
35
+
36
+ Agent frameworks (verifier tools `kya_verify`, `kya_check_action`, `kya_verify_paid`):
37
+
38
+ ```ts
39
+ import { toOpenAI, toVercelAI, toLangChain, toAnthropic } from "@kya-brasil/agent-tools";
40
+ toVercelAI(kya, { tool, jsonSchema }); // from "ai"
41
+ toLangChain(kya, tool); // from "@langchain/core/tools"
42
+ toOpenAI(kya); toAnthropic(kya); // then kya.execute(name, args)
43
+ ```
44
+
45
+ ## Company: give your own agent a credential
46
+
47
+ ```ts
48
+ import { createKyaClient, signChallengeWithEvmKey, signWithECnpj } from "@kya-brasil/agent-tools";
49
+ import { readFileSync } from "node:fs";
50
+
51
+ const kya = createKyaClient({ privateKey: payerPk });
52
+ const reg = await kya.register({ // $0.50
53
+ agent: { kind: "evm", id: agentAddress },
54
+ cnpj: "11.222.333/0001-81",
55
+ mandate: { actions: ["quote", "order", "pay"], max_per_operation: 5000, max_per_day: 20000, currencies: ["BRL", "USDC"] }
56
+ });
57
+
58
+ const agent_signature = await signChallengeWithEvmKey(agentPk, reg.challenge.text);
59
+ const ecnpj_signature = await signWithECnpj({
60
+ certificatePem: readFileSync("ecnpj.crt.pem", "utf8"),
61
+ privateKeyPem: readFileSync("ecnpj.key.pem", "utf8"),
62
+ chainPem: [readFileSync("ecnpj.chain.pem", "utf8")]
63
+ }, reg.challenge.text);
64
+
65
+ const { credential } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature }); // free
66
+ ```
67
+
68
+ From an A1 `.pfx`: `openssl pkcs12 -in cert.pfx -clcerts -nokeys > ecnpj.crt.pem`,
69
+ `openssl pkcs12 -in cert.pfx -nocerts -nodes | openssl pkcs8 -topk8 -nocrypt > ecnpj.key.pem`,
70
+ `openssl pkcs12 -in cert.pfx -cacerts -nokeys > ecnpj.chain.pem`. A3 certificates (token, card) sign through the vendor's software;
71
+ pass the resulting detached CMS as `ecnpj_signature`.
72
+
73
+ Renew every 90 days (`kya.renew` then `kya.renewProve`), revoke at once (`kya.revoke` with `revocationText(id)` signed by the e-CNPJ).
74
+
75
+ ## Prices (USDC)
76
+
77
+ | Call | Price |
78
+ |---|---|
79
+ | verify, verifyByKey, prove, renewProve, revoke, revocations | free |
80
+ | verified (signed receipt, no limit), allowed (signed verdict) | 0.01 |
81
+ | renew | 0.10 |
82
+ | register | 0.50 |
83
+
84
+ Schemas and keys: `/.well-known/kya-mandate.schema.json`, `/.well-known/kya.json`, `/openapi.json`, `/llms.txt`.
85
+ Legal entities only: no CPF is stored; the e-CNPJ signer is recorded by certificate hash.
@@ -0,0 +1,269 @@
1
+ export declare const DEFAULT_BASE_URL = "https://kya-br-agents.marcelfons.workers.dev";
2
+ export interface KyaOptions {
3
+ /** Service origin; defaults to production. */
4
+ baseUrl?: string;
5
+ /** Private key of the paying wallet (USDC on Base). Without it paid calls throw PAYMENT_REQUIRED. */
6
+ privateKey?: `0x${string}`;
7
+ /** Internal channel key (platform partners, billed by invoice); bypasses x402. */
8
+ internalKey?: string;
9
+ fetch?: typeof fetch;
10
+ }
11
+ export type AgentKey = {
12
+ kind: "evm" | "web-bot-auth";
13
+ id: string;
14
+ };
15
+ export type CredentialStatus = "pending" | "active" | "suspended" | "revoked" | "expired";
16
+ export interface Mandate {
17
+ actions?: string[];
18
+ max_per_operation?: number;
19
+ max_per_day?: number;
20
+ currencies?: string[];
21
+ counterparties?: string[];
22
+ valid_from?: string;
23
+ valid_until?: string;
24
+ [k: string]: unknown;
25
+ }
26
+ export interface Attestation {
27
+ algorithm: string;
28
+ key_id: string;
29
+ sha256: string;
30
+ signed_at: string;
31
+ signature: string;
32
+ verify_url?: string;
33
+ }
34
+ export interface Credential {
35
+ credential_id: string;
36
+ version: number;
37
+ agent: AgentKey;
38
+ principal: {
39
+ cnpj: string;
40
+ legal_name?: string;
41
+ registration_status?: string;
42
+ };
43
+ authorized_by: {
44
+ method: string;
45
+ certificate_sha256: string;
46
+ certificate_subject: string;
47
+ issuer: string;
48
+ signed_at: string;
49
+ };
50
+ mandate: Mandate;
51
+ kyb_snapshot: Record<string, unknown>;
52
+ status: CredentialStatus;
53
+ issued_at: string;
54
+ expires_at: string;
55
+ issuer: string;
56
+ verify_url: string;
57
+ revocation_url: string;
58
+ attestation?: Attestation;
59
+ [k: string]: unknown;
60
+ }
61
+ export interface Verification {
62
+ credential_id: string;
63
+ status: CredentialStatus;
64
+ status_reason: string | null;
65
+ agent: AgentKey;
66
+ principal: {
67
+ cnpj: string;
68
+ cnpj_formatted: string;
69
+ legal_name: string | null;
70
+ };
71
+ credential: Credential | null;
72
+ events: {
73
+ ts: string;
74
+ event: string;
75
+ reason: string | null;
76
+ actor: string;
77
+ }[];
78
+ attestation?: Attestation;
79
+ [k: string]: unknown;
80
+ }
81
+ export interface ActionQuery {
82
+ action?: string;
83
+ amount?: number;
84
+ currency?: string;
85
+ counterparty?: string;
86
+ spent_today?: number;
87
+ }
88
+ export interface Verdict {
89
+ credential_id: string;
90
+ status: CredentialStatus;
91
+ allowed: boolean;
92
+ reason: string;
93
+ checks: {
94
+ field: string;
95
+ ok: boolean;
96
+ detail?: string;
97
+ }[];
98
+ not_evaluated: string[];
99
+ attestation?: Attestation;
100
+ [k: string]: unknown;
101
+ }
102
+ export interface Registration {
103
+ credential_id: string;
104
+ agent: AgentKey;
105
+ principal: {
106
+ cnpj: string;
107
+ cnpj_formatted: string;
108
+ };
109
+ mandate: Mandate;
110
+ challenge: {
111
+ id: string;
112
+ text: string;
113
+ expires_at: string;
114
+ };
115
+ next: {
116
+ en: string;
117
+ pt: string;
118
+ };
119
+ mandate_schema: string;
120
+ }
121
+ export interface Proof {
122
+ challenge_id: string;
123
+ agent_signature: string;
124
+ ecnpj_signature: string;
125
+ }
126
+ export declare class KyaError extends Error {
127
+ readonly code: string;
128
+ readonly status: number;
129
+ constructor(code: string, message: string, status: number);
130
+ }
131
+ export interface KyaClient {
132
+ baseUrl: string;
133
+ /** Free (50/day per IP): current credential, live status and event history. */
134
+ verify(credentialId: string): Promise<Verification>;
135
+ /** Free: active credentials held by an agent key. */
136
+ verifyByKey(agent: AgentKey): Promise<{
137
+ agent: AgentKey;
138
+ credentials: Verification[];
139
+ }>;
140
+ /** $0.01, no limit: same as verify plus a signed receipt of the verification. */
141
+ verified(credentialId: string): Promise<Verification & {
142
+ verification: Record<string, unknown>;
143
+ }>;
144
+ /** $0.01: signed verdict, is this action inside the mandate and is the credential active? */
145
+ allowed(credentialId: string, q: ActionQuery): Promise<Verdict>;
146
+ /** $0.50: register an agent for a company; returns the challenge both parties sign. */
147
+ register(input: {
148
+ agent: AgentKey;
149
+ cnpj: string;
150
+ mandate: Mandate;
151
+ }): Promise<Registration>;
152
+ /** Free: send both signatures; the credential is issued if the e-CNPJ, the CNPJ and KYB Brasil all clear. */
153
+ prove(credentialId: string, proof: Proof): Promise<{
154
+ credential: Credential;
155
+ kyb_warnings: string[];
156
+ }>;
157
+ /** $0.10: renewal challenge (then renewProve). */
158
+ renew(credentialId: string): Promise<{
159
+ credential_id: string;
160
+ challenge: {
161
+ id: string;
162
+ text: string;
163
+ expires_at: string;
164
+ };
165
+ }>;
166
+ renewProve(credentialId: string, proof: Proof): Promise<{
167
+ credential: Credential;
168
+ kyb_warnings: string[];
169
+ }>;
170
+ /** Free: the company revokes at once. signed_text must be "KYA Brasil: revoke <id>\n<nonce>", signed with the e-CNPJ. */
171
+ revoke(credentialId: string, input: {
172
+ signed_text: string;
173
+ ecnpj_signature: string;
174
+ reason?: string;
175
+ }): Promise<{
176
+ credential_id: string;
177
+ status: "revoked";
178
+ }>;
179
+ /** Free: credentials revoked since a date (default 30 days). */
180
+ revocations(since?: string): Promise<{
181
+ since: string;
182
+ count: number;
183
+ revocations: {
184
+ credential_id: string;
185
+ revoked_at: string;
186
+ reason: string | null;
187
+ }[];
188
+ }>;
189
+ /** Tool definitions for agent frameworks (verifier side). */
190
+ tools: KyaTool[];
191
+ execute(name: string, args: Record<string, unknown>): Promise<Record<string, unknown>>;
192
+ }
193
+ export interface JsonSchema {
194
+ type: "object";
195
+ properties: Record<string, Record<string, unknown>>;
196
+ required?: string[];
197
+ additionalProperties?: boolean;
198
+ }
199
+ export interface KyaTool {
200
+ name: string;
201
+ description: string;
202
+ priceUsd: number;
203
+ parameters: JsonSchema;
204
+ execute: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
205
+ }
206
+ export declare function createKyaClient(opts?: KyaOptions): KyaClient;
207
+ export declare function toOpenAI(kya: KyaClient): {
208
+ type: "function";
209
+ function: {
210
+ name: string;
211
+ description: string;
212
+ parameters: JsonSchema;
213
+ };
214
+ }[];
215
+ export declare function toVercelAI<T>(kya: KyaClient, ai: {
216
+ tool: (def: {
217
+ description: string;
218
+ parameters: unknown;
219
+ execute: (args: Record<string, unknown>) => Promise<unknown>;
220
+ }) => T;
221
+ jsonSchema: (schema: JsonSchema) => unknown;
222
+ }): Record<string, T>;
223
+ export declare function toLangChain<T>(kya: KyaClient, tool: (fn: (args: Record<string, unknown>) => Promise<string>, def: {
224
+ name: string;
225
+ description: string;
226
+ schema: JsonSchema;
227
+ }) => T): T[];
228
+ export declare function toAnthropic(kya: KyaClient): {
229
+ name: string;
230
+ description: string;
231
+ input_schema: JsonSchema;
232
+ }[];
233
+ /** EIP-191 personal_sign of the challenge text with the agent's EVM key (hex signature). */
234
+ export declare function signChallengeWithEvmKey(privateKey: `0x${string}`, text: string): Promise<string>;
235
+ /** Raw Ed25519 signature (base64url) of the challenge text with the agent's Web Bot Auth key (PKCS#8 base64 or CryptoKey). */
236
+ export declare function signChallengeWithEd25519(privateKey: CryptoKey | string, text: string): Promise<string>;
237
+ export interface ECnpjMaterial {
238
+ /** The e-CNPJ certificate, PEM. */
239
+ certificatePem: string;
240
+ /** Its private key, PEM (PKCS#8). From an A1 .pfx: `openssl pkcs12 -in cert.pfx -nocerts -nodes | openssl pkcs8 -topk8 -nocrypt`. */
241
+ privateKeyPem: string;
242
+ /** Intermediate AC certificates, PEM (the chain up to the ICP-Brasil root). From the .pfx: `openssl pkcs12 -in cert.pfx -cacerts -nokeys`. */
243
+ chainPem?: string[];
244
+ }
245
+ /**
246
+ * Detached CMS/PKCS#7 SignedData (base64 DER) over the challenge text, signed with the company's e-CNPJ.
247
+ * Includes the certificate and the intermediate chain so the issuer can validate it against the ICP-Brasil roots.
248
+ * Works with A1 certificates (file). A3 (token or smart card) must sign through the vendor's software.
249
+ */
250
+ export declare function signWithECnpj(material: ECnpjMaterial, text: string): Promise<string>;
251
+ /** The text the company signs to revoke a credential. */
252
+ export declare function revocationText(credentialId: string, nonce?: string): string;
253
+ export interface VerifyResult {
254
+ valid: boolean;
255
+ reason?: string;
256
+ key_id?: string;
257
+ signed_at?: string;
258
+ }
259
+ /**
260
+ * Verifies the Ed25519 attestation of a credential or a signed answer: recomputes the SHA-256 of the
261
+ * canonical JSON (without attestation and receipt) and checks the signature with the issuer's key
262
+ * (fetched from /.well-known/kya.json unless `publicJwk` is given). This proves the document was issued
263
+ * by KYA Brasil and not modified; the live status still comes from verify().
264
+ */
265
+ export declare function verifyAttestation(doc: Record<string, unknown>, opts?: {
266
+ baseUrl?: string;
267
+ publicJwk?: JsonWebKey;
268
+ fetch?: typeof fetch;
269
+ }): Promise<VerifyResult>;
package/dist/index.js ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @kya-brasil/agent-tools
3
+ *
4
+ * Two sides of KYA Brasil, one client:
5
+ *
6
+ * Verifier (you receive an action from someone else's agent):
7
+ * const kya = createKyaClient({ privateKey }); // wallet optional: free verification works without it
8
+ * const v = await kya.verify(credentialId); // free, 50/day per IP
9
+ * const ok = await kya.allowed(credentialId, { action: "pay", amount: 80, currency: "USDC", counterparty: "33000167000101" }); // $0.01
10
+ * tools for your agent framework: toOpenAI(kya), toVercelAI(kya, { tool, jsonSchema }), toLangChain(kya, tool), toAnthropic(kya)
11
+ *
12
+ * Company (you give your own agent a credential):
13
+ * const reg = await kya.register({ agent: { kind: "evm", id: address }, cnpj, mandate }); // $0.50, returns a challenge
14
+ * const agent_signature = await signChallengeWithEvmKey(agentPrivateKey, reg.challenge.text);
15
+ * const ecnpj_signature = await signWithECnpj({ certificatePem, privateKeyPem, chainPem }, reg.challenge.text);
16
+ * const issued = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature });
17
+ */
18
+ import { x402Client } from "@x402/core/client";
19
+ import { registerExactEvmScheme } from "@x402/evm/exact/client";
20
+ import { toClientEvmSigner } from "@x402/evm";
21
+ import { wrapFetchWithPayment } from "@x402/fetch";
22
+ import { privateKeyToAccount } from "viem/accounts";
23
+ import * as pkijs from "pkijs";
24
+ export const DEFAULT_BASE_URL = "https://kya-br-agents.marcelfons.workers.dev";
25
+ export class KyaError extends Error {
26
+ code;
27
+ status;
28
+ constructor(code, message, status) {
29
+ super(message);
30
+ this.code = code;
31
+ this.status = status;
32
+ this.name = "KyaError";
33
+ }
34
+ }
35
+ function buildFetch(opts) {
36
+ const base = opts.fetch ?? fetch;
37
+ if (!opts.privateKey)
38
+ return base;
39
+ const client = new x402Client();
40
+ registerExactEvmScheme(client, { signer: toClientEvmSigner(privateKeyToAccount(opts.privateKey)) });
41
+ return wrapFetchWithPayment(base, client);
42
+ }
43
+ export function createKyaClient(opts = {}) {
44
+ const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
45
+ const plain = opts.fetch ?? fetch;
46
+ const paying = buildFetch(opts);
47
+ async function call(method, path, body, paid = false, priceUsd = 0) {
48
+ const internal = paid && !!opts.internalKey;
49
+ const url = `${baseUrl}${internal ? path.replace(/^\/v1\//, "/internal/v1/") : path}`;
50
+ const headers = { accept: "application/json" };
51
+ if (body !== undefined)
52
+ headers["content-type"] = "application/json";
53
+ if (internal)
54
+ headers["x-internal-key"] = opts.internalKey;
55
+ const res = await (paid && !internal ? paying : plain)(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
56
+ const json = (await res.json().catch(() => ({})));
57
+ if (res.status === 402)
58
+ throw new KyaError("PAYMENT_REQUIRED", `${method} ${path} costs $${priceUsd.toFixed(2)} USDC; pass privateKey (wallet with USDC on Base) to pay automatically`, 402);
59
+ if (!res.ok)
60
+ throw new KyaError(String(json.error ?? "UPSTREAM_ERROR"), String(json.message ?? `HTTP ${res.status}`), res.status);
61
+ return json;
62
+ }
63
+ const q = (o) => {
64
+ const s = new URLSearchParams();
65
+ for (const [k, v] of Object.entries(o))
66
+ if (v !== undefined && v !== null && v !== "")
67
+ s.set(k, String(v));
68
+ const str = s.toString();
69
+ return str ? `?${str}` : "";
70
+ };
71
+ const client = {
72
+ baseUrl,
73
+ verify: (id) => call("GET", `/v1/agents/${encodeURIComponent(id)}`),
74
+ verifyByKey: (agent) => call("GET", `/v1/agents/by-key${q({ kind: agent.kind, id: agent.id })}`),
75
+ verified: (id) => call("GET", `/v1/agents/${encodeURIComponent(id)}/verified`, undefined, true, 0.01),
76
+ allowed: (id, a) => call("GET", `/v1/agents/${encodeURIComponent(id)}/allowed${q(a)}`, undefined, true, 0.01),
77
+ register: (input) => call("POST", "/v1/agents", input, true, 0.5),
78
+ prove: (id, proof) => call("POST", `/v1/agents/${encodeURIComponent(id)}/prove`, proof),
79
+ renew: (id) => call("POST", `/v1/agents/${encodeURIComponent(id)}/renew`, {}, true, 0.1),
80
+ renewProve: (id, proof) => call("POST", `/v1/agents/${encodeURIComponent(id)}/renew/prove`, proof),
81
+ revoke: (id, input) => call("POST", `/v1/agents/${encodeURIComponent(id)}/revoke`, input),
82
+ revocations: (since) => call("GET", `/v1/revocations${q({ since })}`),
83
+ tools: [],
84
+ execute: async (name, args) => {
85
+ const t = client.tools.find((x) => x.name === name);
86
+ if (!t)
87
+ throw new KyaError("NOT_FOUND", `unknown tool ${name}`, 404);
88
+ return t.execute(args);
89
+ }
90
+ };
91
+ const ID = { type: "string", description: "credential_id of the agent's KYA Brasil credential (the agent sends it with its request)." };
92
+ client.tools = [
93
+ {
94
+ name: "kya_verify", priceUsd: 0,
95
+ 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 or base64url Ed25519 key" } }, additionalProperties: false },
97
+ execute: async (a) => (a.credential_id ? client.verify(String(a.credential_id)) : client.verifyByKey({ kind: a.kind, id: String(a.id) }))
98
+ },
99
+ {
100
+ name: "kya_check_action", priceUsd: 0.01,
101
+ description: "Signed verdict ($0.01): may this agent perform this action (action, amount, currency, counterparty) under its company mandate, and is the credential active right now? Use before accepting an order, a payment or a signature from an agent.",
102
+ 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
+ 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
+ },
105
+ {
106
+ name: "kya_verify_paid", priceUsd: 0.01,
107
+ description: "Same as kya_verify without the daily limit, plus a signed receipt of the verification ($0.01).",
108
+ parameters: { type: "object", properties: { credential_id: ID }, required: ["credential_id"], additionalProperties: false },
109
+ execute: async (a) => client.verified(String(a.credential_id))
110
+ }
111
+ ];
112
+ return client;
113
+ }
114
+ // --- Adapters (no framework dependency: you pass the framework's constructors in) ---
115
+ export function toOpenAI(kya) {
116
+ return kya.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
117
+ }
118
+ export function toVercelAI(kya, ai) {
119
+ return Object.fromEntries(kya.tools.map((t) => [t.name, ai.tool({ description: t.description, parameters: ai.jsonSchema(t.parameters), execute: (args) => t.execute(args) })]));
120
+ }
121
+ export function toLangChain(kya, tool) {
122
+ return kya.tools.map((t) => tool(async (args) => JSON.stringify(await t.execute(args)), { name: t.name, description: t.description, schema: t.parameters }));
123
+ }
124
+ export function toAnthropic(kya) {
125
+ return kya.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
126
+ }
127
+ // --- Signing the challenge (company side) ---
128
+ /** EIP-191 personal_sign of the challenge text with the agent's EVM key (hex signature). */
129
+ export async function signChallengeWithEvmKey(privateKey, text) {
130
+ return privateKeyToAccount(privateKey).signMessage({ message: text });
131
+ }
132
+ /** Raw Ed25519 signature (base64url) of the challenge text with the agent's Web Bot Auth key (PKCS#8 base64 or CryptoKey). */
133
+ export async function signChallengeWithEd25519(privateKey, text) {
134
+ const key = typeof privateKey === "string" ? await crypto.subtle.importKey("pkcs8", Uint8Array.from(atob(privateKey), (c) => c.charCodeAt(0)), { name: "Ed25519" }, false, ["sign"]) : privateKey;
135
+ const sig = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, new TextEncoder().encode(text)));
136
+ return btoa(String.fromCharCode(...sig)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
137
+ }
138
+ const pemToDer = (pem) => Uint8Array.from(atob(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")), (c) => c.charCodeAt(0)).buffer;
139
+ const pemBlocks = (pem) => [...pem.matchAll(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g)].map((m) => m[0]);
140
+ /**
141
+ * Detached CMS/PKCS#7 SignedData (base64 DER) over the challenge text, signed with the company's e-CNPJ.
142
+ * Includes the certificate and the intermediate chain so the issuer can validate it against the ICP-Brasil roots.
143
+ * Works with A1 certificates (file). A3 (token or smart card) must sign through the vendor's software.
144
+ */
145
+ export async function signWithECnpj(material, text) {
146
+ pkijs.setEngine("kya-agent-tools", new pkijs.CryptoEngine({ name: "kya-agent-tools", crypto: globalThis.crypto }));
147
+ const leaf = pkijs.Certificate.fromBER(pemToDer(material.certificatePem));
148
+ const chain = (material.chainPem ?? []).flatMap(pemBlocks).map((p) => pkijs.Certificate.fromBER(pemToDer(p)));
149
+ const der = pemToDer(material.privateKeyPem);
150
+ const algo = leaf.subjectPublicKeyInfo.algorithm.algorithmId === "1.2.840.10045.2.1" ? { name: "ECDSA", namedCurve: "P-256" } : { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
151
+ const key = await crypto.subtle.importKey("pkcs8", der, algo, false, ["sign"]);
152
+ const signed = new pkijs.SignedData({
153
+ version: 1,
154
+ encapContentInfo: new pkijs.EncapsulatedContentInfo({ eContentType: "1.2.840.113549.1.7.1" }),
155
+ signerInfos: [new pkijs.SignerInfo({ version: 1, sid: new pkijs.IssuerAndSerialNumber({ issuer: leaf.issuer, serialNumber: leaf.serialNumber }) })],
156
+ certificates: [leaf, ...chain]
157
+ });
158
+ await signed.sign(key, 0, "SHA-256", new TextEncoder().encode(text).buffer);
159
+ const ci = new pkijs.ContentInfo({ contentType: "1.2.840.113549.1.7.2", content: signed.toSchema(true) });
160
+ const bytes = new Uint8Array(ci.toSchema().toBER(false));
161
+ let s = "";
162
+ for (const b of bytes)
163
+ s += String.fromCharCode(b);
164
+ return btoa(s);
165
+ }
166
+ /** The text the company signs to revoke a credential. */
167
+ export function revocationText(credentialId, nonce = crypto.randomUUID()) {
168
+ return `KYA Brasil: revoke ${credentialId}\n${nonce}`;
169
+ }
170
+ // --- Local verification of a credential or a signed answer (no network once you hold the key) ---
171
+ function canonicalize(v) {
172
+ if (v === null || typeof v !== "object")
173
+ return JSON.stringify(v);
174
+ if (Array.isArray(v))
175
+ return `[${v.map(canonicalize).join(",")}]`;
176
+ const o = v;
177
+ return `{${Object.keys(o).filter((k) => o[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
178
+ }
179
+ /**
180
+ * Verifies the Ed25519 attestation of a credential or a signed answer: recomputes the SHA-256 of the
181
+ * canonical JSON (without attestation and receipt) and checks the signature with the issuer's key
182
+ * (fetched from /.well-known/kya.json unless `publicJwk` is given). This proves the document was issued
183
+ * by KYA Brasil and not modified; the live status still comes from verify().
184
+ */
185
+ export async function verifyAttestation(doc, opts = {}) {
186
+ const att = doc.attestation;
187
+ if (!att || att.algorithm !== "ed25519" || !att.sha256 || !att.signed_at || !att.signature || !att.key_id)
188
+ return { valid: false, reason: "no ed25519 attestation in the document" };
189
+ let jwk = opts.publicJwk;
190
+ if (!jwk) {
191
+ const base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
192
+ const keys = (await (await (opts.fetch ?? fetch)(`${base}/.well-known/kya.json`)).json());
193
+ jwk = keys.keys?.find((k) => k.key_id === att.key_id)?.jwk;
194
+ if (!jwk)
195
+ return { valid: false, reason: `key ${att.key_id} not published by ${base}`, key_id: att.key_id, signed_at: att.signed_at };
196
+ }
197
+ const { attestation: _a, receipt: _r, ...payload } = doc;
198
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalize(payload)));
199
+ const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
200
+ if (hex !== att.sha256)
201
+ return { valid: false, reason: "content hash mismatch: the document was modified", key_id: att.key_id, signed_at: att.signed_at };
202
+ const key = await crypto.subtle.importKey("jwk", jwk, { name: "Ed25519" }, true, ["verify"]);
203
+ const sig = Uint8Array.from(atob(att.signature), (c) => c.charCodeAt(0));
204
+ const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, sig, new TextEncoder().encode(`${hex}\n${att.signed_at}`));
205
+ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@kya-brasil/agent-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.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.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/celfons/kya-brasil.git",
22
+ "directory": "packages/agent-tools"
23
+ },
24
+ "homepage": "https://kya-br-agents.marcelfons.workers.dev",
25
+ "bugs": {
26
+ "url": "https://github.com/celfons/kya-brasil/issues"
27
+ },
28
+ "author": "Marcel Oliveira",
29
+ "keywords": [
30
+ "kya",
31
+ "know-your-agent",
32
+ "agent-identity",
33
+ "brazil",
34
+ "cnpj",
35
+ "e-cnpj",
36
+ "icp-brasil",
37
+ "x402",
38
+ "mcp",
39
+ "ai-agents",
40
+ "vercel-ai",
41
+ "langchain",
42
+ "openai",
43
+ "anthropic"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "typecheck": "tsc -p tsconfig.json",
51
+ "test": "vitest run"
52
+ },
53
+ "dependencies": {
54
+ "@x402/core": "^2.28.0",
55
+ "@x402/evm": "^2.28.0",
56
+ "@x402/fetch": "^2.28.0",
57
+ "pkijs": "^3.4.1",
58
+ "viem": "^2.57.5"
59
+ },
60
+ "devDependencies": {
61
+ "@peculiar/asn1-schema": "^2.3.15",
62
+ "@peculiar/asn1-x509": "^2.3.15",
63
+ "@peculiar/x509": "^2.1.0",
64
+ "reflect-metadata": "^0.2.2",
65
+ "typescript": "^5.9.3",
66
+ "vitest": "^3.2.4",
67
+ "@kya-br/core": "workspace:*"
68
+ }
69
+ }