@kya-brasil/agent-tools 0.8.0 → 0.10.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 +20 -0
- package/dist/cli.js +65 -4
- package/dist/index.d.ts +42 -7
- package/dist/index.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,6 +100,22 @@ const { response, callee } = await me.fetch("https://beta.com.br/orders", { meth
|
|
|
100
100
|
if (!callee?.ok) throw new Error("unsigned or invalid callee"); // callee.kya: principal, mandate, status
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
## Person: an agent that acts for you (e-CPF)
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const reg = await kya.register({ agent: { kind: "evm", id: agentAddress }, principal: "person", mandate }); // $0.25
|
|
107
|
+
// agent signs reg.challenge.text; you sign it with your e-CPF (reg.sign_url, kya-sign or signWithECnpjPfx: same code)
|
|
108
|
+
const { credential } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature: ecpfSignature });
|
|
109
|
+
// credential.principal = { kind: "person", verified_by: "icp-brasil-e-cpf" }: nothing personal
|
|
110
|
+
|
|
111
|
+
// A verifier that already has the person's CPF:
|
|
112
|
+
const m = await kya.principalMatch(credentialId, "529.982.247-25"); // $0.01, signed: { match, subject_ref? }
|
|
113
|
+
|
|
114
|
+
// The person, to see their agents and every check made on them:
|
|
115
|
+
const text = personAgentsText(); // sign it with the e-CPF
|
|
116
|
+
const mine = await kya.personAgents({ signed_text: text, ecpf_signature });
|
|
117
|
+
```
|
|
118
|
+
|
|
103
119
|
## Company: give your own agent a credential
|
|
104
120
|
|
|
105
121
|
```ts
|
|
@@ -159,6 +175,10 @@ The PIN comes from `KYA_TOKEN_PIN` or is asked for. The key never leaves the tok
|
|
|
159
175
|
software PKCS#11 token) in CI. The signing code is in `@kya-brasil/core` (`openPfx`, `ecnpjSigner`,
|
|
160
176
|
`buildDetachedCms`), re-exported here; any other token or cloud signer plugs into `ecnpjSigner`.
|
|
161
177
|
|
|
178
|
+
**Cloud certificate (PSC).** `kya-sign --cloud ./my-psc.mjs --link "<sign_url>"` with a module that implements
|
|
179
|
+
`CloudSignatureProvider` for your PSC ([template](https://github.com/celfons/kya-brasil/tree/main/examples/cloud-provider));
|
|
180
|
+
the browser opens for the approval and the terminal receives the redirect on `127.0.0.1`.
|
|
181
|
+
|
|
162
182
|
**From the agent's side**, `kya.signLink(credentialId, { challenge_id, agent_signature })` (free) returns the
|
|
163
183
|
`sign_url` to send, the same as `signingLink(...)` offline and the MCP tool `kya_sign_link`.
|
|
164
184
|
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
* A1 file: kya-sign --pfx empresa.pfx --link "<sign_url>&agent_signature=…"
|
|
5
5
|
* A3 token: kya-sign --pkcs11 /usr/lib/libeTPkcs11.so --link "<sign_url>&agent_signature=…"
|
|
6
6
|
* kya-sign --pkcs11 <library> --list (certificates on the token)
|
|
7
|
+
* Cloud: kya-sign --cloud ./my-psc.mjs --link "<sign_url>&agent_signature=…"
|
|
8
|
+
* (a module exporting a CloudSignatureProvider: yours, for your PSC; the browser opens for the
|
|
9
|
+
* approval and comes back to a one-time local address)
|
|
7
10
|
* Text instead of a link: --text-file desafio.txt or --text "<text>" (prints the signature).
|
|
8
11
|
* The password (A1) comes from KYA_PFX_PASSWORD and the PIN (A3) from KYA_TOKEN_PIN, or are asked for without
|
|
9
12
|
* echo. With a link carrying agent_signature, the proof is sent and the issued credential printed. Everything
|
|
10
13
|
* happens on this machine; the certificate and its password or PIN are never sent anywhere.
|
|
11
14
|
*/
|
|
12
15
|
import { readFileSync } from "node:fs";
|
|
13
|
-
import {
|
|
16
|
+
import { createServer } from "node:http";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { cloudSigner, newState, pkcePair, openPfx, readSigningLink } from "@kya-brasil/core";
|
|
14
21
|
import { listTokenCertificates, openToken } from "./pkcs11.js";
|
|
15
22
|
const args = process.argv.slice(2);
|
|
16
23
|
const opt = (name) => {
|
|
@@ -46,17 +53,69 @@ async function askSecret(envName, prompt) {
|
|
|
46
53
|
});
|
|
47
54
|
});
|
|
48
55
|
}
|
|
56
|
+
/** Loads a provider module: a path (./my-psc.mjs) or a package name; default export or `provider`. */
|
|
57
|
+
async function loadProvider(spec) {
|
|
58
|
+
const url = spec.startsWith(".") || spec.startsWith("/") ? pathToFileURL(resolve(spec)).href : spec;
|
|
59
|
+
const mod = (await import(url));
|
|
60
|
+
const p = (mod.provider ?? mod.default);
|
|
61
|
+
if (!p || typeof p.authorizationUrl !== "function" || typeof p.exchangeCode !== "function")
|
|
62
|
+
die(`${spec} does not export a CloudSignatureProvider (default export or "provider")`);
|
|
63
|
+
return p;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* OAuth with PKCE through the browser: a one-time server on 127.0.0.1 receives the code, checks the state,
|
|
67
|
+
* exchanges it and returns an e-CNPJ signer over the provider's session.
|
|
68
|
+
*/
|
|
69
|
+
async function cloudLogin(provider, port, open) {
|
|
70
|
+
const state = newState();
|
|
71
|
+
const { verifier, challenge } = await pkcePair();
|
|
72
|
+
let answer;
|
|
73
|
+
const answered = new Promise((r) => (answer = r));
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
76
|
+
if (u.pathname !== "/callback")
|
|
77
|
+
return void res.writeHead(404).end();
|
|
78
|
+
const ok = u.searchParams.get("state") === state && !!u.searchParams.get("code");
|
|
79
|
+
res.writeHead(ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" }).end(ok ? "<p>Autorizado. Pode fechar esta janela e voltar ao terminal.</p>" : "<p>Autorização recusada ou inválida.</p>");
|
|
80
|
+
answer(ok ? { code: u.searchParams.get("code") } : { error: u.searchParams.get("error_description") ?? u.searchParams.get("error") ?? "state mismatch" });
|
|
81
|
+
});
|
|
82
|
+
await new Promise((r) => server.listen(port, "127.0.0.1", r));
|
|
83
|
+
const addr = server.address();
|
|
84
|
+
const redirectUri = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : port}/callback`;
|
|
85
|
+
const timer = setTimeout(() => answer({ error: "no approval within 5 minutes" }), 300_000);
|
|
86
|
+
try {
|
|
87
|
+
const url = await provider.authorizationUrl({ redirectUri, state, codeChallenge: challenge });
|
|
88
|
+
process.stderr.write(`Abra para aprovar no ${provider.name} / open to approve:\n ${url}\n`);
|
|
89
|
+
if (open) {
|
|
90
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
91
|
+
const a = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
92
|
+
spawn(cmd, a, { stdio: "ignore", detached: true }).on("error", () => undefined).unref();
|
|
93
|
+
}
|
|
94
|
+
const r = await answered;
|
|
95
|
+
if (!r.code)
|
|
96
|
+
throw new Error(`not authorized: ${r.error}`);
|
|
97
|
+
const session = await provider.exchangeCode({ code: r.code, redirectUri, codeVerifier: verifier });
|
|
98
|
+
const signer = await cloudSigner(session);
|
|
99
|
+
return { ...signer, close: async () => void (await session.close?.().catch(() => undefined)) };
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
server.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
49
106
|
const USAGE = `usage:
|
|
50
107
|
kya-sign --pfx <file.pfx> (--link <signing link> | --text-file <file> | --text <text>)
|
|
51
108
|
kya-sign --pkcs11 <library> [--slot N] [--label L] (--link … | --text-file … | --text …)
|
|
52
109
|
kya-sign --pkcs11 <library> --list
|
|
110
|
+
kya-sign --cloud <provider module> [--port N] [--no-open] (--link … | --text-file … | --text …)
|
|
53
111
|
`;
|
|
54
112
|
async function main() {
|
|
55
113
|
if (args.includes("--help") || args.includes("-h"))
|
|
56
114
|
return void process.stdout.write(USAGE);
|
|
57
115
|
const pfxPath = opt("pfx");
|
|
58
116
|
const library = opt("pkcs11");
|
|
59
|
-
|
|
117
|
+
const cloud = opt("cloud");
|
|
118
|
+
if (!pfxPath && !library && !cloud) {
|
|
60
119
|
process.stdout.write(USAGE);
|
|
61
120
|
process.exit(2);
|
|
62
121
|
}
|
|
@@ -69,7 +128,9 @@ async function main() {
|
|
|
69
128
|
const link = opt("link") ? readSigningLink(opt("link")) : null;
|
|
70
129
|
const text = link ? link.text : opt("text-file") ? readFileSync(opt("text-file"), "utf8") : (opt("text") ?? die("give --link, --text-file or --text"));
|
|
71
130
|
let signer;
|
|
72
|
-
if (
|
|
131
|
+
if (cloud)
|
|
132
|
+
signer = await cloudLogin(await loadProvider(cloud), opt("port") ? Number(opt("port")) : 0, !args.includes("--no-open"));
|
|
133
|
+
else if (library)
|
|
73
134
|
signer = await openToken({ library, slot, label: opt("label"), pin: await askSecret("KYA_TOKEN_PIN", "PIN do token / token PIN: ") });
|
|
74
135
|
else
|
|
75
136
|
signer = await openPfx(new Uint8Array(readFileSync(pfxPath)), await askSecret("KYA_PFX_PASSWORD", "Senha do certificado / certificate password: "));
|
|
@@ -90,7 +151,7 @@ async function main() {
|
|
|
90
151
|
process.exitCode = 1;
|
|
91
152
|
}
|
|
92
153
|
finally {
|
|
93
|
-
signer.close?.();
|
|
154
|
+
await signer.close?.();
|
|
94
155
|
}
|
|
95
156
|
}
|
|
96
157
|
main().catch((e) => die(e.message));
|
package/dist/index.d.ts
CHANGED
|
@@ -49,10 +49,13 @@ export interface Credential {
|
|
|
49
49
|
credential_id: string;
|
|
50
50
|
version: number;
|
|
51
51
|
agent: AgentKey;
|
|
52
|
+
/** A company, or (D-29) a person: { kind: "person", verified_by: "icp-brasil-e-cpf" } with nothing personal. */
|
|
52
53
|
principal: {
|
|
53
|
-
|
|
54
|
+
kind?: "company" | "person";
|
|
55
|
+
cnpj?: string;
|
|
54
56
|
legal_name?: string;
|
|
55
57
|
registration_status?: string;
|
|
58
|
+
verified_by?: string;
|
|
56
59
|
};
|
|
57
60
|
authorized_by: {
|
|
58
61
|
method: string;
|
|
@@ -79,9 +82,11 @@ export interface Verification {
|
|
|
79
82
|
status_reason: string | null;
|
|
80
83
|
agent: AgentKey;
|
|
81
84
|
principal: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
kind?: "person";
|
|
86
|
+
verified_by?: string;
|
|
87
|
+
cnpj?: string;
|
|
88
|
+
cnpj_formatted?: string;
|
|
89
|
+
legal_name?: string | null;
|
|
85
90
|
};
|
|
86
91
|
credential: Credential | null;
|
|
87
92
|
events: {
|
|
@@ -127,8 +132,9 @@ export interface Registration {
|
|
|
127
132
|
credential_id: string;
|
|
128
133
|
agent: AgentKey;
|
|
129
134
|
principal: {
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
kind?: "person";
|
|
136
|
+
cnpj?: string;
|
|
137
|
+
cnpj_formatted?: string;
|
|
132
138
|
};
|
|
133
139
|
mandate: Mandate;
|
|
134
140
|
challenge: {
|
|
@@ -195,11 +201,39 @@ export interface KyaClient {
|
|
|
195
201
|
/** $0.01: signed verdict, is this action inside the mandate and is the credential active? */
|
|
196
202
|
allowed(credentialId: string, q: ActionQuery): Promise<Verdict>;
|
|
197
203
|
/** $0.25: register an agent for a company; returns the challenge both parties sign. */
|
|
204
|
+
/** $0.25: register an agent for a company (cnpj) or, with principal "person", for a natural person who signs with an e-CPF (D-29). */
|
|
198
205
|
register(input: {
|
|
199
206
|
agent: AgentKey;
|
|
200
|
-
cnpj
|
|
207
|
+
cnpj?: string;
|
|
208
|
+
principal?: "company" | "person";
|
|
201
209
|
mandate: Mandate;
|
|
202
210
|
}): Promise<Registration>;
|
|
211
|
+
/** $0.01 (D-29): does the CPF you already have match the person behind this credential? On a match, your own subject_ref for that person. Never reveals the CPF. */
|
|
212
|
+
principalMatch(credentialId: string, cpf: string): Promise<{
|
|
213
|
+
credential_id: string;
|
|
214
|
+
match: boolean;
|
|
215
|
+
subject_ref?: string;
|
|
216
|
+
verifier: string;
|
|
217
|
+
checked_at: string;
|
|
218
|
+
attestation?: Attestation;
|
|
219
|
+
}>;
|
|
220
|
+
/** Free (D-29): the person's own credentials and the CPF checks made on each; sign personAgentsText() with the e-CPF. */
|
|
221
|
+
personAgents(input: {
|
|
222
|
+
signed_text: string;
|
|
223
|
+
ecpf_signature: string;
|
|
224
|
+
}): Promise<{
|
|
225
|
+
agents: {
|
|
226
|
+
credential_id: string;
|
|
227
|
+
agent: AgentKey;
|
|
228
|
+
status: string;
|
|
229
|
+
expires_at: string | null;
|
|
230
|
+
checks: {
|
|
231
|
+
verifier: string;
|
|
232
|
+
matched: boolean;
|
|
233
|
+
at: string;
|
|
234
|
+
}[];
|
|
235
|
+
}[];
|
|
236
|
+
}>;
|
|
203
237
|
/** Free: send both signatures; the credential is issued if the e-CNPJ is valid and not revoked, the CNPJ matches and the company is ATIVA. */
|
|
204
238
|
prove(credentialId: string, proof: Proof): Promise<{
|
|
205
239
|
credential: Credential; /** D-24: e.g. registration status not confirmed (issued anyway). */
|
|
@@ -373,6 +407,7 @@ export declare function verifyCredentialOffline(credential: Credential, opts?: {
|
|
|
373
407
|
now?: Date;
|
|
374
408
|
}): Promise<OfflineCheck>;
|
|
375
409
|
export * from "./auth.js";
|
|
410
|
+
export { isValidCpf, personAgentsText } from "@kya-brasil/core";
|
|
376
411
|
export { buildDetachedCms, cnpjFromCommonName, ecnpjSigner, openPfx, readSigningLink, signWithECnpjPfx, type ECnpjSigner, type RawSigner } from "@kya-brasil/core";
|
|
377
412
|
export { listTokenCertificates, openToken, type TokenCertificate, type TokenOptions } from "./pkcs11.js";
|
|
378
413
|
/**
|
package/dist/index.js
CHANGED
|
@@ -94,6 +94,8 @@ export function createKyaClient(opts = {}) {
|
|
|
94
94
|
revoke: (id, input) => call("POST", `/v1/agents/${encodeURIComponent(id)}/revoke`, input),
|
|
95
95
|
revocations: (since) => call("GET", `/v1/revocations${q({ since })}`),
|
|
96
96
|
companyAgents: (cnpj, input) => call("POST", `/v1/companies/${encodeURIComponent(cnpj)}/agents`, input),
|
|
97
|
+
principalMatch: (id, cpf) => call("POST", `/v1/agents/${encodeURIComponent(id)}/principal-match`, { cpf }, true, 0.01),
|
|
98
|
+
personAgents: (input) => call("POST", "/v1/people/agents", input),
|
|
97
99
|
setCompanyWebhook: (cnpj, input) => call("POST", `/v1/companies/${encodeURIComponent(cnpj)}/webhook`, input),
|
|
98
100
|
tools: [],
|
|
99
101
|
execute: async (name, args) => {
|
|
@@ -252,6 +254,7 @@ export async function verifyCredentialOffline(credential, opts = {}) {
|
|
|
252
254
|
}
|
|
253
255
|
export * from "./auth.js";
|
|
254
256
|
// e-CNPJ signing (D-26, D-27): the implementation lives in @kya-brasil/core, shared with anyone using the library.
|
|
257
|
+
export { isValidCpf, personAgentsText } from "@kya-brasil/core";
|
|
255
258
|
export { buildDetachedCms, cnpjFromCommonName, ecnpjSigner, openPfx, readSigningLink, signWithECnpjPfx } from "@kya-brasil/core";
|
|
256
259
|
export { listTokenCertificates, openToken } from "./pkcs11.js";
|
|
257
260
|
import { signingLink as coreSigningLink } from "@kya-brasil/core";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-brasil/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "KYA Brasil for AI agents: verify that an agent acts for a Brazilian company (e-CNPJ-signed credential, revocation and company registration checked daily against the Receita's open data, no third-party service), check an action against its mandate, or give your own agent a credential (sign with the company's e-CNPJ: A1 .pfx or A3 token via PKCS#11, in code or with the kya-sign command). 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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@x402/fetch": "^2.28.0",
|
|
57
57
|
"pkijs": "^3.4.1",
|
|
58
58
|
"viem": "^2.57.5",
|
|
59
|
-
"@kya-brasil/core": "^0.
|
|
59
|
+
"@kya-brasil/core": "^0.10.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@peculiar/asn1-schema": "^2.3.15",
|