@kya-brasil/agent-tools 0.0.0-stage → 0.2.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 +95 -2
- package/dist/index.d.ts +339 -0
- package/dist/index.js +220 -0
- package/package.json +67 -4
package/README.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kya-brasil/agent-tools
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
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
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Also check that the key that signed the agent's request is `v.agent.id`; the credential_id alone proves nothing.
|
|
39
|
+
|
|
40
|
+
Agent frameworks (verifier tools `kya_verify`, `kya_check_action`, `kya_register_act`, `kya_verify_paid`):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { toOpenAI, toVercelAI, toLangChain, toAnthropic } from "@kya-brasil/agent-tools";
|
|
44
|
+
toVercelAI(kya, { tool, jsonSchema }); // from "ai"
|
|
45
|
+
toLangChain(kya, tool); // from "@langchain/core/tools"
|
|
46
|
+
toOpenAI(kya); toAnthropic(kya); // then kya.execute(name, args)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Company: give your own agent a credential
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createKyaClient, signChallengeWithEvmKey, signWithECnpj } from "@kya-brasil/agent-tools";
|
|
53
|
+
import { readFileSync } from "node:fs";
|
|
54
|
+
|
|
55
|
+
const kya = createKyaClient({ privateKey: payerPk });
|
|
56
|
+
const reg = await kya.register({ // $0.50
|
|
57
|
+
agent: { kind: "evm", id: agentAddress },
|
|
58
|
+
cnpj: "11.222.333/0001-81",
|
|
59
|
+
mandate: {
|
|
60
|
+
actions: ["quote", "order", "pay"],
|
|
61
|
+
max_per_operation: { amount: 5000, currency: "BRL" },
|
|
62
|
+
max_per_day: { amount: 20000, currency: "BRL" },
|
|
63
|
+
currencies: ["BRL"],
|
|
64
|
+
counterparties: ["12345678000190", "*.beta.com.br"]
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const agent_signature = await signChallengeWithEvmKey(agentPk, reg.challenge.text);
|
|
69
|
+
const ecnpj_signature = await signWithECnpj({
|
|
70
|
+
certificatePem: readFileSync("ecnpj.crt.pem", "utf8"),
|
|
71
|
+
privateKeyPem: readFileSync("ecnpj.key.pem", "utf8"),
|
|
72
|
+
chainPem: [readFileSync("ecnpj.chain.pem", "utf8")]
|
|
73
|
+
}, reg.challenge.text);
|
|
74
|
+
|
|
75
|
+
const { credential } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature }); // free
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
From an A1 `.pfx`: `openssl pkcs12 -in cert.pfx -clcerts -nokeys > ecnpj.crt.pem`,
|
|
79
|
+
`openssl pkcs12 -in cert.pfx -nocerts -nodes | openssl pkcs8 -topk8 -nocrypt > ecnpj.key.pem`,
|
|
80
|
+
`openssl pkcs12 -in cert.pfx -cacerts -nokeys > ecnpj.chain.pem`. A3 certificates (token, card) sign through the vendor's software;
|
|
81
|
+
pass the resulting detached CMS as `ecnpj_signature`.
|
|
82
|
+
|
|
83
|
+
Renew every 90 days (`kya.renew` then `kya.renewProve`), revoke at once (`kya.revoke` with `revocationText(id)` signed by the e-CNPJ).
|
|
84
|
+
|
|
85
|
+
## Prices (USDC)
|
|
86
|
+
|
|
87
|
+
| Call | Price |
|
|
88
|
+
|---|---|
|
|
89
|
+
| verify, verifyByKey, prove, renewProve, revoke, revocations | free |
|
|
90
|
+
| verified (signed receipt, no limit), allowed (signed verdict) | 0.01 |
|
|
91
|
+
| registerAct (signed receipt of what the agent did) | 0.02 |
|
|
92
|
+
| renew | 0.10 |
|
|
93
|
+
| register | 0.50 |
|
|
94
|
+
|
|
95
|
+
Schemas and keys: `/.well-known/kya-mandate.schema.json`, `/.well-known/kya.json`, `/openapi.json`, `/llms.txt`.
|
|
96
|
+
Legal entities only: no CPF is stored; the e-CNPJ signer is recorded by certificate hash.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
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
|
+
/** erc8004: id is eip155:<chainId>:<identityRegistry>:<agentId>; the on-chain agentWallet signs (credential.agent.wallet). */
|
|
12
|
+
export type AgentKey = {
|
|
13
|
+
kind: "evm" | "web-bot-auth" | "erc8004";
|
|
14
|
+
id: string;
|
|
15
|
+
wallet?: `0x${string}`;
|
|
16
|
+
};
|
|
17
|
+
export type CredentialStatus = "pending" | "active" | "suspended" | "revoked" | "expired";
|
|
18
|
+
export interface Mandate {
|
|
19
|
+
actions?: string[];
|
|
20
|
+
max_per_operation?: {
|
|
21
|
+
amount: number;
|
|
22
|
+
currency: string;
|
|
23
|
+
};
|
|
24
|
+
max_per_day?: {
|
|
25
|
+
amount: number;
|
|
26
|
+
currency: string;
|
|
27
|
+
};
|
|
28
|
+
currencies?: string[];
|
|
29
|
+
counterparties?: string[];
|
|
30
|
+
valid_from?: string;
|
|
31
|
+
valid_until?: string;
|
|
32
|
+
[k: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface Attestation {
|
|
35
|
+
algorithm: string;
|
|
36
|
+
key_id: string;
|
|
37
|
+
sha256: string;
|
|
38
|
+
signed_at: string;
|
|
39
|
+
signature: string;
|
|
40
|
+
verify_url?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface Credential {
|
|
43
|
+
credential_id: string;
|
|
44
|
+
version: number;
|
|
45
|
+
agent: AgentKey;
|
|
46
|
+
principal: {
|
|
47
|
+
cnpj: string;
|
|
48
|
+
legal_name?: string;
|
|
49
|
+
registration_status?: string;
|
|
50
|
+
};
|
|
51
|
+
authorized_by: {
|
|
52
|
+
method: string;
|
|
53
|
+
certificate_sha256: string;
|
|
54
|
+
certificate_subject: string;
|
|
55
|
+
issuer: string;
|
|
56
|
+
signed_at: string;
|
|
57
|
+
};
|
|
58
|
+
mandate: Mandate;
|
|
59
|
+
kyb_snapshot: Record<string, unknown>;
|
|
60
|
+
status: CredentialStatus;
|
|
61
|
+
issued_at: string;
|
|
62
|
+
expires_at: string;
|
|
63
|
+
issuer: string;
|
|
64
|
+
verify_url: string;
|
|
65
|
+
revocation_url: string;
|
|
66
|
+
attestation?: Attestation;
|
|
67
|
+
[k: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
export interface Verification {
|
|
70
|
+
credential_id: string;
|
|
71
|
+
status: CredentialStatus;
|
|
72
|
+
status_reason: string | null;
|
|
73
|
+
agent: AgentKey;
|
|
74
|
+
principal: {
|
|
75
|
+
cnpj: string;
|
|
76
|
+
cnpj_formatted: string;
|
|
77
|
+
legal_name: string | null;
|
|
78
|
+
};
|
|
79
|
+
credential: Credential | null;
|
|
80
|
+
events: {
|
|
81
|
+
ts: string;
|
|
82
|
+
event: string;
|
|
83
|
+
reason: string | null;
|
|
84
|
+
actor: string;
|
|
85
|
+
}[];
|
|
86
|
+
attestation?: Attestation;
|
|
87
|
+
[k: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
export interface ActionQuery {
|
|
90
|
+
action?: string;
|
|
91
|
+
amount?: number;
|
|
92
|
+
currency?: string;
|
|
93
|
+
counterparty?: string;
|
|
94
|
+
spent_today?: number;
|
|
95
|
+
}
|
|
96
|
+
export interface Verdict {
|
|
97
|
+
credential_id: string;
|
|
98
|
+
status: CredentialStatus;
|
|
99
|
+
allowed: boolean;
|
|
100
|
+
reason: string;
|
|
101
|
+
checks: {
|
|
102
|
+
field: string;
|
|
103
|
+
ok: boolean;
|
|
104
|
+
detail?: string;
|
|
105
|
+
}[];
|
|
106
|
+
not_evaluated: string[];
|
|
107
|
+
attestation?: Attestation;
|
|
108
|
+
[k: string]: unknown;
|
|
109
|
+
}
|
|
110
|
+
export interface Registration {
|
|
111
|
+
credential_id: string;
|
|
112
|
+
agent: AgentKey;
|
|
113
|
+
principal: {
|
|
114
|
+
cnpj: string;
|
|
115
|
+
cnpj_formatted: string;
|
|
116
|
+
};
|
|
117
|
+
mandate: Mandate;
|
|
118
|
+
challenge: {
|
|
119
|
+
id: string;
|
|
120
|
+
text: string;
|
|
121
|
+
expires_at: string;
|
|
122
|
+
};
|
|
123
|
+
next: {
|
|
124
|
+
en: string;
|
|
125
|
+
pt: string;
|
|
126
|
+
};
|
|
127
|
+
mandate_schema: string;
|
|
128
|
+
}
|
|
129
|
+
export interface Proof {
|
|
130
|
+
challenge_id: string;
|
|
131
|
+
agent_signature: string;
|
|
132
|
+
ecnpj_signature: string;
|
|
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
|
+
}
|
|
173
|
+
export declare class KyaError extends Error {
|
|
174
|
+
readonly code: string;
|
|
175
|
+
readonly status: number;
|
|
176
|
+
constructor(code: string, message: string, status: number);
|
|
177
|
+
}
|
|
178
|
+
export interface KyaClient {
|
|
179
|
+
baseUrl: string;
|
|
180
|
+
/** Free (50/day per IP): current credential, live status and event history. */
|
|
181
|
+
verify(credentialId: string): Promise<Verification>;
|
|
182
|
+
/** Free: active credentials held by an agent key. */
|
|
183
|
+
verifyByKey(agent: AgentKey): Promise<{
|
|
184
|
+
agent: AgentKey;
|
|
185
|
+
credentials: Verification[];
|
|
186
|
+
}>;
|
|
187
|
+
/** $0.01, no limit: same as verify plus a signed receipt of the verification. */
|
|
188
|
+
verified(credentialId: string): Promise<Verification & {
|
|
189
|
+
verification: Record<string, unknown>;
|
|
190
|
+
}>;
|
|
191
|
+
/** $0.01: signed verdict, is this action inside the mandate and is the credential active? */
|
|
192
|
+
allowed(credentialId: string, q: ActionQuery): Promise<Verdict>;
|
|
193
|
+
/** $0.50: register an agent for a company; returns the challenge both parties sign. */
|
|
194
|
+
register(input: {
|
|
195
|
+
agent: AgentKey;
|
|
196
|
+
cnpj: string;
|
|
197
|
+
mandate: Mandate;
|
|
198
|
+
}): Promise<Registration>;
|
|
199
|
+
/** Free: send both signatures; the credential is issued if the e-CNPJ, the CNPJ and KYB Brasil all clear. */
|
|
200
|
+
prove(credentialId: string, proof: Proof): Promise<{
|
|
201
|
+
credential: Credential;
|
|
202
|
+
kyb_warnings: string[];
|
|
203
|
+
}>;
|
|
204
|
+
/** $0.10: renewal challenge (then renewProve). */
|
|
205
|
+
renew(credentialId: string): Promise<{
|
|
206
|
+
credential_id: string;
|
|
207
|
+
challenge: {
|
|
208
|
+
id: string;
|
|
209
|
+
text: string;
|
|
210
|
+
expires_at: string;
|
|
211
|
+
};
|
|
212
|
+
}>;
|
|
213
|
+
renewProve(credentialId: string, proof: Proof): Promise<{
|
|
214
|
+
credential: Credential;
|
|
215
|
+
kyb_warnings: string[];
|
|
216
|
+
}>;
|
|
217
|
+
/** Free: the company revokes at once. signed_text must be "KYA Brasil: revoke <id>\n<nonce>", signed with the e-CNPJ. */
|
|
218
|
+
revoke(credentialId: string, input: {
|
|
219
|
+
signed_text: string;
|
|
220
|
+
ecnpj_signature: string;
|
|
221
|
+
reason?: string;
|
|
222
|
+
}): Promise<{
|
|
223
|
+
credential_id: string;
|
|
224
|
+
status: "revoked";
|
|
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
|
+
}>;
|
|
247
|
+
/** Free: credentials revoked since a date (default 30 days). */
|
|
248
|
+
revocations(since?: string): Promise<{
|
|
249
|
+
since: string;
|
|
250
|
+
count: number;
|
|
251
|
+
revocations: {
|
|
252
|
+
credential_id: string;
|
|
253
|
+
revoked_at: string;
|
|
254
|
+
reason: string | null;
|
|
255
|
+
}[];
|
|
256
|
+
}>;
|
|
257
|
+
/** Tool definitions for agent frameworks (verifier side). */
|
|
258
|
+
tools: KyaTool[];
|
|
259
|
+
execute(name: string, args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
260
|
+
}
|
|
261
|
+
export interface JsonSchema {
|
|
262
|
+
type: "object";
|
|
263
|
+
properties: Record<string, Record<string, unknown>>;
|
|
264
|
+
required?: string[];
|
|
265
|
+
additionalProperties?: boolean;
|
|
266
|
+
}
|
|
267
|
+
export interface KyaTool {
|
|
268
|
+
name: string;
|
|
269
|
+
description: string;
|
|
270
|
+
priceUsd: number;
|
|
271
|
+
parameters: JsonSchema;
|
|
272
|
+
execute: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
273
|
+
}
|
|
274
|
+
export declare function createKyaClient(opts?: KyaOptions): KyaClient;
|
|
275
|
+
export declare function toOpenAI(kya: KyaClient): {
|
|
276
|
+
type: "function";
|
|
277
|
+
function: {
|
|
278
|
+
name: string;
|
|
279
|
+
description: string;
|
|
280
|
+
parameters: JsonSchema;
|
|
281
|
+
};
|
|
282
|
+
}[];
|
|
283
|
+
export declare function toVercelAI<T>(kya: KyaClient, ai: {
|
|
284
|
+
tool: (def: {
|
|
285
|
+
description: string;
|
|
286
|
+
parameters: unknown;
|
|
287
|
+
execute: (args: Record<string, unknown>) => Promise<unknown>;
|
|
288
|
+
}) => T;
|
|
289
|
+
jsonSchema: (schema: JsonSchema) => unknown;
|
|
290
|
+
}): Record<string, T>;
|
|
291
|
+
export declare function toLangChain<T>(kya: KyaClient, tool: (fn: (args: Record<string, unknown>) => Promise<string>, def: {
|
|
292
|
+
name: string;
|
|
293
|
+
description: string;
|
|
294
|
+
schema: JsonSchema;
|
|
295
|
+
}) => T): T[];
|
|
296
|
+
export declare function toAnthropic(kya: KyaClient): {
|
|
297
|
+
name: string;
|
|
298
|
+
description: string;
|
|
299
|
+
input_schema: JsonSchema;
|
|
300
|
+
}[];
|
|
301
|
+
/** EIP-191 personal_sign of the challenge text with the agent's EVM key (hex signature). */
|
|
302
|
+
export declare function signChallengeWithEvmKey(privateKey: `0x${string}`, text: string): Promise<string>;
|
|
303
|
+
/** Raw Ed25519 signature (base64url) of the challenge text with the agent's Web Bot Auth key (PKCS#8 base64 or CryptoKey). */
|
|
304
|
+
export declare function signChallengeWithEd25519(privateKey: CryptoKey | string, text: string): Promise<string>;
|
|
305
|
+
export interface ECnpjMaterial {
|
|
306
|
+
/** The e-CNPJ certificate, PEM. */
|
|
307
|
+
certificatePem: string;
|
|
308
|
+
/** Its private key, PEM (PKCS#8). From an A1 .pfx: `openssl pkcs12 -in cert.pfx -nocerts -nodes | openssl pkcs8 -topk8 -nocrypt`. */
|
|
309
|
+
privateKeyPem: string;
|
|
310
|
+
/** Intermediate AC certificates, PEM (the chain up to the ICP-Brasil root). From the .pfx: `openssl pkcs12 -in cert.pfx -cacerts -nokeys`. */
|
|
311
|
+
chainPem?: string[];
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Detached CMS/PKCS#7 SignedData (base64 DER) over the challenge text, signed with the company's e-CNPJ.
|
|
315
|
+
* Includes the certificate and the intermediate chain so the issuer can validate it against the ICP-Brasil roots.
|
|
316
|
+
* Works with A1 certificates (file). A3 (token or smart card) must sign through the vendor's software.
|
|
317
|
+
*/
|
|
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;
|
|
321
|
+
/** The text the company signs to revoke a credential. */
|
|
322
|
+
export declare function revocationText(credentialId: string, nonce?: string): string;
|
|
323
|
+
export interface VerifyResult {
|
|
324
|
+
valid: boolean;
|
|
325
|
+
reason?: string;
|
|
326
|
+
key_id?: string;
|
|
327
|
+
signed_at?: string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Verifies the Ed25519 attestation of a credential or a signed answer: recomputes the SHA-256 of the
|
|
331
|
+
* canonical JSON (without attestation and receipt) and checks the signature with the issuer's key
|
|
332
|
+
* (fetched from /.well-known/kya.json unless `publicJwk` is given). This proves the document was issued
|
|
333
|
+
* by KYA Brasil and not modified; the live status still comes from verify().
|
|
334
|
+
*/
|
|
335
|
+
export declare function verifyAttestation(doc: Record<string, unknown>, opts?: {
|
|
336
|
+
baseUrl?: string;
|
|
337
|
+
publicJwk?: JsonWebKey;
|
|
338
|
+
fetch?: typeof fetch;
|
|
339
|
+
}): Promise<VerifyResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
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),
|
|
87
|
+
tools: [],
|
|
88
|
+
execute: async (name, args) => {
|
|
89
|
+
const t = client.tools.find((x) => x.name === name);
|
|
90
|
+
if (!t)
|
|
91
|
+
throw new KyaError("NOT_FOUND", `unknown tool ${name}`, 404);
|
|
92
|
+
return t.execute(args);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const ID = { type: "string", description: "credential_id of the agent's KYA Brasil credential (the agent sends it with its request)." };
|
|
96
|
+
client.tools = [
|
|
97
|
+
{
|
|
98
|
+
name: "kya_verify", priceUsd: 0,
|
|
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.",
|
|
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 },
|
|
101
|
+
execute: async (a) => (a.credential_id ? client.verify(String(a.credential_id)) : client.verifyByKey({ kind: a.kind, id: String(a.id) }))
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "kya_check_action", priceUsd: 0.01,
|
|
105
|
+
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.",
|
|
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 },
|
|
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 })
|
|
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
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "kya_verify_paid", priceUsd: 0.01,
|
|
117
|
+
description: "Same as kya_verify without the daily limit, plus a signed receipt of the verification ($0.01).",
|
|
118
|
+
parameters: { type: "object", properties: { credential_id: ID }, required: ["credential_id"], additionalProperties: false },
|
|
119
|
+
execute: async (a) => client.verified(String(a.credential_id))
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
return client;
|
|
123
|
+
}
|
|
124
|
+
// --- Adapters (no framework dependency: you pass the framework's constructors in) ---
|
|
125
|
+
export function toOpenAI(kya) {
|
|
126
|
+
return kya.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
127
|
+
}
|
|
128
|
+
export function toVercelAI(kya, ai) {
|
|
129
|
+
return Object.fromEntries(kya.tools.map((t) => [t.name, ai.tool({ description: t.description, parameters: ai.jsonSchema(t.parameters), execute: (args) => t.execute(args) })]));
|
|
130
|
+
}
|
|
131
|
+
export function toLangChain(kya, tool) {
|
|
132
|
+
return kya.tools.map((t) => tool(async (args) => JSON.stringify(await t.execute(args)), { name: t.name, description: t.description, schema: t.parameters }));
|
|
133
|
+
}
|
|
134
|
+
export function toAnthropic(kya) {
|
|
135
|
+
return kya.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
|
|
136
|
+
}
|
|
137
|
+
// --- Signing the challenge (company side) ---
|
|
138
|
+
/** EIP-191 personal_sign of the challenge text with the agent's EVM key (hex signature). */
|
|
139
|
+
export async function signChallengeWithEvmKey(privateKey, text) {
|
|
140
|
+
return privateKeyToAccount(privateKey).signMessage({ message: text });
|
|
141
|
+
}
|
|
142
|
+
/** Raw Ed25519 signature (base64url) of the challenge text with the agent's Web Bot Auth key (PKCS#8 base64 or CryptoKey). */
|
|
143
|
+
export async function signChallengeWithEd25519(privateKey, text) {
|
|
144
|
+
const key = typeof privateKey === "string" ? await crypto.subtle.importKey("pkcs8", Uint8Array.from(atob(privateKey), (c) => c.charCodeAt(0)), { name: "Ed25519" }, false, ["sign"]) : privateKey;
|
|
145
|
+
const sig = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, new TextEncoder().encode(text)));
|
|
146
|
+
return btoa(String.fromCharCode(...sig)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
147
|
+
}
|
|
148
|
+
const pemToDer = (pem) => Uint8Array.from(atob(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")), (c) => c.charCodeAt(0)).buffer;
|
|
149
|
+
const pemBlocks = (pem) => [...pem.matchAll(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g)].map((m) => m[0]);
|
|
150
|
+
/**
|
|
151
|
+
* Detached CMS/PKCS#7 SignedData (base64 DER) over the challenge text, signed with the company's e-CNPJ.
|
|
152
|
+
* Includes the certificate and the intermediate chain so the issuer can validate it against the ICP-Brasil roots.
|
|
153
|
+
* Works with A1 certificates (file). A3 (token or smart card) must sign through the vendor's software.
|
|
154
|
+
*/
|
|
155
|
+
export async function signWithECnpj(material, text) {
|
|
156
|
+
pkijs.setEngine("kya-agent-tools", new pkijs.CryptoEngine({ name: "kya-agent-tools", crypto: globalThis.crypto }));
|
|
157
|
+
const leaf = pkijs.Certificate.fromBER(pemToDer(material.certificatePem));
|
|
158
|
+
const chain = (material.chainPem ?? []).flatMap(pemBlocks).map((p) => pkijs.Certificate.fromBER(pemToDer(p)));
|
|
159
|
+
const der = pemToDer(material.privateKeyPem);
|
|
160
|
+
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" };
|
|
161
|
+
const key = await crypto.subtle.importKey("pkcs8", der, algo, false, ["sign"]);
|
|
162
|
+
const signed = new pkijs.SignedData({
|
|
163
|
+
version: 1,
|
|
164
|
+
encapContentInfo: new pkijs.EncapsulatedContentInfo({ eContentType: "1.2.840.113549.1.7.1" }),
|
|
165
|
+
signerInfos: [new pkijs.SignerInfo({ version: 1, sid: new pkijs.IssuerAndSerialNumber({ issuer: leaf.issuer, serialNumber: leaf.serialNumber }) })],
|
|
166
|
+
certificates: [leaf, ...chain]
|
|
167
|
+
});
|
|
168
|
+
await signed.sign(key, 0, "SHA-256", new TextEncoder().encode(text).buffer);
|
|
169
|
+
const ci = new pkijs.ContentInfo({ contentType: "1.2.840.113549.1.7.2", content: signed.toSchema(true) });
|
|
170
|
+
const bytes = new Uint8Array(ci.toSchema().toBER(false));
|
|
171
|
+
let s = "";
|
|
172
|
+
for (const b of bytes)
|
|
173
|
+
s += String.fromCharCode(b);
|
|
174
|
+
return btoa(s);
|
|
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
|
+
}
|
|
180
|
+
/** The text the company signs to revoke a credential. */
|
|
181
|
+
export function revocationText(credentialId, nonce = crypto.randomUUID()) {
|
|
182
|
+
return `KYA Brasil: revoke ${credentialId}\n${nonce}`;
|
|
183
|
+
}
|
|
184
|
+
// --- Local verification of a credential or a signed answer (no network once you hold the key) ---
|
|
185
|
+
function canonicalize(v) {
|
|
186
|
+
if (v === null || typeof v !== "object")
|
|
187
|
+
return JSON.stringify(v);
|
|
188
|
+
if (Array.isArray(v))
|
|
189
|
+
return `[${v.map(canonicalize).join(",")}]`;
|
|
190
|
+
const o = v;
|
|
191
|
+
return `{${Object.keys(o).filter((k) => o[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(o[k])}`).join(",")}}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Verifies the Ed25519 attestation of a credential or a signed answer: recomputes the SHA-256 of the
|
|
195
|
+
* canonical JSON (without attestation and receipt) and checks the signature with the issuer's key
|
|
196
|
+
* (fetched from /.well-known/kya.json unless `publicJwk` is given). This proves the document was issued
|
|
197
|
+
* by KYA Brasil and not modified; the live status still comes from verify().
|
|
198
|
+
*/
|
|
199
|
+
export async function verifyAttestation(doc, opts = {}) {
|
|
200
|
+
const att = doc.attestation;
|
|
201
|
+
if (!att || att.algorithm !== "ed25519" || !att.sha256 || !att.signed_at || !att.signature || !att.key_id)
|
|
202
|
+
return { valid: false, reason: "no ed25519 attestation in the document" };
|
|
203
|
+
let jwk = opts.publicJwk;
|
|
204
|
+
if (!jwk) {
|
|
205
|
+
const base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
206
|
+
const keys = (await (await (opts.fetch ?? fetch)(`${base}/.well-known/kya.json`)).json());
|
|
207
|
+
jwk = keys.keys?.find((k) => k.key_id === att.key_id)?.jwk;
|
|
208
|
+
if (!jwk)
|
|
209
|
+
return { valid: false, reason: `key ${att.key_id} not published by ${base}`, key_id: att.key_id, signed_at: att.signed_at };
|
|
210
|
+
}
|
|
211
|
+
const { attestation: _a, receipt: _r, ...payload } = doc;
|
|
212
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalize(payload)));
|
|
213
|
+
const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
214
|
+
if (hex !== att.sha256)
|
|
215
|
+
return { valid: false, reason: "content hash mismatch: the document was modified", key_id: att.key_id, signed_at: att.signed_at };
|
|
216
|
+
const key = await crypto.subtle.importKey("jwk", jwk, { name: "Ed25519" }, true, ["verify"]);
|
|
217
|
+
const sig = Uint8Array.from(atob(att.signature), (c) => c.charCodeAt(0));
|
|
218
|
+
const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, sig, new TextEncoder().encode(`${hex}\n${att.signed_at}`));
|
|
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 };
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-brasil/agent-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.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
|
+
}
|