@kya-brasil/agent-tools 0.5.4 → 0.7.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
@@ -19,7 +19,7 @@ Node 18+ (global `fetch`, WebCrypto). ESM.
19
19
  ## Verifier: you receive an action from someone else's agent
20
20
 
21
21
  ```ts
22
- import { createKyaClient, verifyAttestation, verifyCredentialOffline, companyText, signWithECnpj } from "@kya-brasil/agent-tools";
22
+ import { createKyaClient, verifyAttestation, verifyCredentialOffline, companyText, signWithECnpj, hashPaymentRef } from "@kya-brasil/agent-tools";
23
23
 
24
24
  const kya = createKyaClient({ privateKey: process.env.KYA_PAYER_PK as `0x${string}` }); // wallet optional
25
25
 
@@ -34,6 +34,8 @@ if (!verdict.allowed) reject(verdict.reason);
34
34
  await verifyAttestation(verdict); // local Ed25519 check of the issuer's signature
35
35
 
36
36
  // max_per_day: pass your own spent_today; the issuer does not record acts.
37
+ // Payment instrument (card networks, acquirers, D-25): pass the hash of the token reference or PAR you received,
38
+ // never the card number: { payment_network: "visa", payment_ref_sha256: await hashPaymentRef(tokenRef) }
37
39
 
38
40
  const offline = await verifyCredentialOffline(credential); // issuer down: signature + expires_at only,
39
41
  // live_status "unknown" (your risk until expires_at)
@@ -101,7 +103,7 @@ if (!callee?.ok) throw new Error("unsigned or invalid callee"); // callee.
101
103
  ## Company: give your own agent a credential
102
104
 
103
105
  ```ts
104
- import { createKyaClient, signChallengeWithEvmKey, signWithECnpj } from "@kya-brasil/agent-tools";
106
+ import { createKyaClient, signChallengeWithEvmKey, signingLink, signWithECnpjPfx } from "@kya-brasil/agent-tools";
105
107
  import { readFileSync } from "node:fs";
106
108
 
107
109
  const kya = createKyaClient({ privateKey: payerPk });
@@ -118,19 +120,29 @@ const reg = await kya.register({ // $0
118
120
  });
119
121
 
120
122
  const agent_signature = await signChallengeWithEvmKey(agentPk, reg.challenge.text);
121
- const ecnpj_signature = await signWithECnpj({
122
- certificatePem: readFileSync("ecnpj.crt.pem", "utf8"),
123
- privateKeyPem: readFileSync("ecnpj.key.pem", "utf8"),
124
- chainPem: [readFileSync("ecnpj.chain.pem", "utf8")]
125
- }, reg.challenge.text);
126
123
 
127
- const { credential } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature }); // free
124
+ // Usual case: a person holds the e-CNPJ (owner, accountant). Send them the link; they sign in the browser
125
+ // (.pfx, or a .p7s from any ICP-Brasil signer for A3 tokens and cloud certificates) and the credential is issued.
126
+ const link = signingLink({ credentialId: reg.credential_id, challenge: reg.challenge, agentSignature: agent_signature });
127
+ // (reg.sign_url is the same link without the agent signature)
128
+
129
+ // Or in code, when you hold the A1 file yourself:
130
+ const ecnpj_signature = signWithECnpjPfx(readFileSync("empresa.pfx"), process.env.PFX_PASSWORD!, reg.challenge.text);
131
+ const { credential, warnings } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature }); // free
128
132
  ```
129
133
 
130
- From an A1 `.pfx`: `openssl pkcs12 -in cert.pfx -clcerts -nokeys > ecnpj.crt.pem`,
131
- `openssl pkcs12 -in cert.pfx -nocerts -nodes | openssl pkcs8 -topk8 -nocrypt > ecnpj.key.pem`,
132
- `openssl pkcs12 -in cert.pfx -cacerts -nokeys > ecnpj.chain.pem`. A3 certificates (token, card) sign through the vendor's software;
133
- pass the resulting detached CMS as `ecnpj_signature`.
134
+ `signWithECnpjPfx` reads the `.pfx`/`.p12` directly, including the legacy 3DES/RC2 encryption many Brazilian A1
135
+ files use; `signWithECnpj` still takes PEM. On a terminal, the `kya-sign` command does the same without code:
136
+
137
+ ```sh
138
+ npx -p @kya-brasil/agent-tools kya-sign --pfx empresa.pfx --link "<sign_url>&agent_signature=…" # signs and issues
139
+ npx -p @kya-brasil/agent-tools kya-sign --pfx empresa.pfx --text-file desafio.txt # prints the signature
140
+ ```
141
+
142
+ The password comes from `KYA_PFX_PASSWORD` or is asked for without echo. Everything runs locally.
143
+
144
+ An agent should not hold the company's e-CNPJ to sign for itself: that signature is the company's consent.
145
+ Let the agent prepare and send the link, and a person sign.
134
146
 
135
147
  Renew every 90 days (`kya.renew` then `kya.renewProve`), revoke at once (`kya.revoke` with `revocationText(id)` signed by the e-CNPJ).
136
148
 
package/dist/auth.d.ts CHANGED
@@ -80,8 +80,8 @@ export interface LocalVerdict {
80
80
  }
81
81
  /**
82
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 nothing of it either: pass your own spent_today to ctx.allowed.
83
+ * (with `*.` wildcards), payment_instruments (when you pass payment_ref_sha256), valid_from / valid_until, and
84
+ * max_per_day when you pass spent_today (nobody else counts it: the issuer keeps no record of acts).
85
85
  */
86
86
  export declare function checkMandate(mandate: Mandate, q: ActionQuery, now?: Date): LocalVerdict;
87
87
  export interface KyaAuth {
package/dist/auth.js CHANGED
@@ -73,7 +73,7 @@ export async function signRequest(p) {
73
73
  headers[KYA_HEADERS.content] = contentSha256;
74
74
  return headers;
75
75
  }
76
- const RESERVED = ["actions", "max_per_operation", "max_per_day", "currencies", "counterparties", "valid_from", "valid_until"];
76
+ const RESERVED = ["actions", "max_per_operation", "max_per_day", "currencies", "counterparties", "payment_instruments", "valid_from", "valid_until"];
77
77
  function normalizeCounterparty(c) {
78
78
  const t = c.trim();
79
79
  const digits = t.replace(/\D/g, "");
@@ -88,8 +88,8 @@ function normalizeCounterparty(c) {
88
88
  }
89
89
  /**
90
90
  * Local evaluation of the reserved mandate fields: actions, max_per_operation, currencies, counterparties
91
- * (with `*.` wildcards), valid_from / valid_until. `max_per_day` is listed as not evaluated: only the issuer
92
- * knows nothing of it either: pass your own spent_today to ctx.allowed.
91
+ * (with `*.` wildcards), payment_instruments (when you pass payment_ref_sha256), valid_from / valid_until, and
92
+ * max_per_day when you pass spent_today (nobody else counts it: the issuer keeps no record of acts).
93
93
  */
94
94
  export function checkMandate(mandate, q, now = new Date()) {
95
95
  const checks = [];
@@ -116,9 +116,21 @@ export function checkMandate(mandate, q, now = new Date()) {
116
116
  });
117
117
  push("counterparties", ok, `${c} ${ok ? "is" : "is not"} an allowed counterparty`);
118
118
  }
119
+ if (mandate.max_per_day && q.amount !== undefined && q.spent_today !== undefined) {
120
+ const total = q.amount + q.spent_today;
121
+ const same = !q.currency || q.currency.toUpperCase() === mandate.max_per_day.currency;
122
+ push("max_per_day", same && total <= mandate.max_per_day.amount, same ? `${total} today (incl. this one) vs ceiling ${mandate.max_per_day.amount} ${mandate.max_per_day.currency}` : `ceiling is in ${mandate.max_per_day.currency}, operation in ${q.currency}`);
123
+ }
124
+ const instruments = Array.isArray(mandate.payment_instruments) ? mandate.payment_instruments : [];
125
+ if (instruments.length && q.payment_ref_sha256) {
126
+ const h = q.payment_ref_sha256.trim().toLowerCase();
127
+ const net = q.payment_network?.trim().toLowerCase();
128
+ const hit = instruments.find((p) => p.ref_sha256 === h && (!net || p.network === net));
129
+ push("payment_instruments", !!hit, hit ? `${hit.network} instrument ${h.slice(0, 12)}… is authorized` : `instrument ${net ? `${net} ` : ""}${h.slice(0, 12)}… is not in the mandate`);
130
+ }
119
131
  const not_evaluated = Object.keys(mandate).filter((k) => !RESERVED.includes(k));
120
- if (mandate.max_per_day)
121
- not_evaluated.push("max_per_day (only the issuer knows today's registered total)");
132
+ if (mandate.max_per_day && q.spent_today === undefined)
133
+ not_evaluated.push("max_per_day (pass spent_today: the issuer keeps no record of acts)");
122
134
  return { allowed: checks.every((c) => c.ok), checks, not_evaluated };
123
135
  }
124
136
  const headerOf = (headers, name) => {
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * kya-sign: sign a KYA challenge (or a company request text) with the e-CNPJ in a .pfx / .p12 file.
4
+ * npx -p @kya-brasil/agent-tools kya-sign --pfx empresa.pfx --text-file desafio.txt
5
+ * npx -p @kya-brasil/agent-tools kya-sign --pfx empresa.pfx --link "https://…/sign?id=…&challenge_id=…&text=…&agent_signature=…"
6
+ * The password comes from KYA_PFX_PASSWORD or is asked for (not echoed). Prints the base64 signature, or with
7
+ * --link and an agent_signature in it, sends the proof and prints the issued credential.
8
+ * Everything happens on this machine; the certificate is never sent anywhere.
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { openPfx } from "./pfx.js";
12
+ const args = process.argv.slice(2);
13
+ const opt = (name) => {
14
+ const i = args.indexOf(`--${name}`);
15
+ return i >= 0 ? args[i + 1] : undefined;
16
+ };
17
+ const die = (msg) => {
18
+ process.stderr.write(`kya-sign: ${msg}\n`);
19
+ process.exit(1);
20
+ };
21
+ async function askPassword() {
22
+ if (process.env.KYA_PFX_PASSWORD !== undefined)
23
+ return process.env.KYA_PFX_PASSWORD;
24
+ if (!process.stdin.isTTY)
25
+ die("set KYA_PFX_PASSWORD (no terminal to ask for the password)");
26
+ process.stderr.write("Senha do certificado / certificate password: ");
27
+ process.stdin.setRawMode(true);
28
+ process.stdin.resume();
29
+ let pw = "";
30
+ return new Promise((resolve) => {
31
+ process.stdin.on("data", (b) => {
32
+ for (const ch of b.toString("utf8")) {
33
+ if (ch === "\r" || ch === "\n") {
34
+ process.stdin.setRawMode(false);
35
+ process.stdin.pause();
36
+ process.stderr.write("\n");
37
+ return resolve(pw);
38
+ }
39
+ if (ch === "\u0003")
40
+ process.exit(130);
41
+ pw = ch === "\u007f" ? pw.slice(0, -1) : pw + ch;
42
+ }
43
+ });
44
+ });
45
+ }
46
+ const fromB64u = (s) => new TextDecoder().decode(Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)));
47
+ async function main() {
48
+ if (args.includes("--help") || args.includes("-h") || !opt("pfx")) {
49
+ process.stdout.write("usage: kya-sign --pfx <file.pfx> (--text-file <file> | --text <text> | --link <signing link>)\n");
50
+ process.exit(args.includes("--help") || args.includes("-h") ? 0 : 2);
51
+ }
52
+ const link = opt("link") ? new URL(opt("link")) : null;
53
+ const text = link ? fromB64u(link.searchParams.get("text") ?? die("the link has no text")) : opt("text-file") ? readFileSync(opt("text-file"), "utf8") : (opt("text") ?? die("give --text-file, --text or --link"));
54
+ const signer = openPfx(new Uint8Array(readFileSync(opt("pfx"))), await askPassword());
55
+ const cnpj = /^cnpj: (\d{14})$/m.exec(text)?.[1];
56
+ process.stderr.write(`certificate: ${signer.subject}\n`);
57
+ if (cnpj && signer.cnpj && cnpj !== signer.cnpj)
58
+ die(`this certificate is for CNPJ ${signer.cnpj}, the text is for ${cnpj}`);
59
+ process.stderr.write(`signing:\n${text.split("\n").map((l) => ` ${l}`).join("\n")}\n`);
60
+ const signature = signer.sign(text);
61
+ const id = link?.searchParams.get("id");
62
+ const challengeId = link?.searchParams.get("challenge_id");
63
+ const agentSignature = link?.searchParams.get("agent_signature");
64
+ if (!link || !id || !challengeId || !agentSignature) {
65
+ process.stdout.write(`${signature}\n`);
66
+ return;
67
+ }
68
+ const res = await fetch(new URL(`/v1/agents/${encodeURIComponent(id)}/prove`, link.origin), {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({ challenge_id: challengeId, agent_signature: agentSignature, ecnpj_signature: signature })
72
+ });
73
+ const body = await res.text();
74
+ process.stdout.write(`${body}\n`);
75
+ if (!res.ok)
76
+ process.exit(1);
77
+ }
78
+ main().catch((e) => die(e.message));
package/dist/index.d.ts CHANGED
@@ -27,6 +27,12 @@ export interface Mandate {
27
27
  };
28
28
  currencies?: string[];
29
29
  counterparties?: string[];
30
+ /** D-25: instruments the agent may use, as SHA-256 of the network's token reference or PAR (see hashPaymentRef). */
31
+ payment_instruments?: {
32
+ network: string;
33
+ ref_sha256: string;
34
+ label?: string;
35
+ }[];
30
36
  valid_from?: string;
31
37
  valid_until?: string;
32
38
  [k: string]: unknown;
@@ -93,7 +99,16 @@ export interface ActionQuery {
93
99
  currency?: string;
94
100
  counterparty?: string;
95
101
  spent_today?: number;
102
+ /** D-25: network of the payment instrument used (visa, mastercard, elo, pix, ...). */
103
+ payment_network?: string;
104
+ /** D-25: SHA-256 hex of its token reference or PAR (hashPaymentRef); never a card number. */
105
+ payment_ref_sha256?: string;
96
106
  }
107
+ /**
108
+ * SHA-256 (lowercase hex) of a payment reference as mandates store it (D-25): UTF-8, whitespace removed, case
109
+ * kept. Use it on a network token reference or PAR, never on a card number.
110
+ */
111
+ export declare function hashPaymentRef(ref: string): Promise<string>;
97
112
  export interface Verdict {
98
113
  credential_id: string;
99
114
  status: CredentialStatus;
@@ -346,3 +361,19 @@ export declare function verifyCredentialOffline(credential: Credential, opts?: {
346
361
  now?: Date;
347
362
  }): Promise<OfflineCheck>;
348
363
  export * from "./auth.js";
364
+ export * from "./pfx.js";
365
+ /**
366
+ * Link to the issuer's signing page (<issuer>/sign) for the person who holds the company's e-CNPJ, usually the
367
+ * owner or the accountant. The agent registers, signs its half and sends this link; the person opens it, sees
368
+ * which agent, company and mandate they are authorizing, signs with the .pfx in the browser (the file never
369
+ * leaves the computer) or uploads a .p7s from any ICP-Brasil signer, and the credential is issued.
370
+ */
371
+ export declare function signingLink(p: {
372
+ credentialId: string;
373
+ challenge: {
374
+ id: string;
375
+ text: string;
376
+ };
377
+ agentSignature?: string;
378
+ baseUrl?: string;
379
+ }): string;
package/dist/index.js CHANGED
@@ -23,6 +23,17 @@ import { privateKeyToAccount } from "viem/accounts";
23
23
  import * as pkijs from "pkijs";
24
24
  import { toBase64Url } from "./base64url.js";
25
25
  export const DEFAULT_BASE_URL = "https://kya-br-agents.marcelfons.workers.dev";
26
+ /**
27
+ * SHA-256 (lowercase hex) of a payment reference as mandates store it (D-25): UTF-8, whitespace removed, case
28
+ * kept. Use it on a network token reference or PAR, never on a card number.
29
+ */
30
+ export async function hashPaymentRef(ref) {
31
+ const clean = ref.replace(/\s+/g, "");
32
+ if (!clean)
33
+ throw new Error("empty payment reference");
34
+ const d = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(clean)));
35
+ return Array.from(d, (b) => b.toString(16).padStart(2, "0")).join("");
36
+ }
26
37
  export class KyaError extends Error {
27
38
  code;
28
39
  status;
@@ -102,8 +113,8 @@ export function createKyaClient(opts = {}) {
102
113
  {
103
114
  name: "kya_check_action", priceUsd: 0.01,
104
115
  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.",
105
- 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 },
106
- 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 })
116
+ 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" }, payment_network: { type: "string", description: "visa, mastercard, elo, pix, ..." }, payment_ref_sha256: { type: "string", description: "SHA-256 hex of the payment token reference or PAR used (never a card number)" } }, required: ["credential_id"], additionalProperties: false },
117
+ 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, payment_network: a.payment_network, payment_ref_sha256: a.payment_ref_sha256 })
107
118
  },
108
119
  {
109
120
  name: "kya_verify_paid", priceUsd: 0.01,
@@ -239,3 +250,19 @@ export async function verifyCredentialOffline(credential, opts = {}) {
239
250
  return { signature, within_validity, live_status: "unknown", usable_offline: signature.valid && within_validity && credential.status === "active" };
240
251
  }
241
252
  export * from "./auth.js";
253
+ export * from "./pfx.js";
254
+ /**
255
+ * Link to the issuer's signing page (<issuer>/sign) for the person who holds the company's e-CNPJ, usually the
256
+ * owner or the accountant. The agent registers, signs its half and sends this link; the person opens it, sees
257
+ * which agent, company and mandate they are authorizing, signs with the .pfx in the browser (the file never
258
+ * leaves the computer) or uploads a .p7s from any ICP-Brasil signer, and the credential is issued.
259
+ */
260
+ export function signingLink(p) {
261
+ const u = new URL("/sign", p.baseUrl ?? DEFAULT_BASE_URL);
262
+ u.searchParams.set("id", p.credentialId);
263
+ u.searchParams.set("challenge_id", p.challenge.id);
264
+ u.searchParams.set("text", toBase64Url(new TextEncoder().encode(p.challenge.text)));
265
+ if (p.agentSignature)
266
+ u.searchParams.set("agent_signature", p.agentSignature);
267
+ return u.toString();
268
+ }
package/dist/pfx.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export interface PfxSigner {
2
+ /** Subject of the e-CNPJ certificate, e.g. "CN=ACME LTDA:11222333000181, ...". */
3
+ subject: string;
4
+ /** CNPJ read from the certificate's common name ("NAME:CNPJ"), when present. */
5
+ cnpj: string | null;
6
+ notAfter: Date;
7
+ /** Detached CMS/PKCS#7 SignedData (base64 DER) over `text`, with the certificate chain found in the file. */
8
+ sign(text: string): string;
9
+ }
10
+ /** Opens a PKCS#12 file (bytes) with its password; throws a readable error on a wrong password or an empty file. */
11
+ export declare function openPfx(pfx: Uint8Array, password: string): PfxSigner;
12
+ /** One call: sign `text` with the e-CNPJ in a .pfx/.p12 file. Returns the base64 DER the issuer expects as ecnpj_signature. */
13
+ export declare function signWithECnpjPfx(pfx: Uint8Array, password: string, text: string): string;
package/dist/pfx.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * e-CNPJ signing straight from the .pfx / .p12 file companies actually have (A1 certificates). node-forge reads
3
+ * the legacy PKCS#12 encryptions (3DES, RC2) that Web Crypto cannot, which many Brazilian A1 files still use.
4
+ * The page at <issuer>/sign runs the same steps in the browser; keep the two in sync.
5
+ */
6
+ import forge from "node-forge";
7
+ const OID = {
8
+ shroudedKeyBag: forge.pki.oids.pkcs8ShroudedKeyBag,
9
+ keyBag: forge.pki.oids.keyBag,
10
+ certBag: forge.pki.oids.certBag,
11
+ sha256: forge.pki.oids.sha256,
12
+ contentType: forge.pki.oids.contentType,
13
+ data: forge.pki.oids.data,
14
+ messageDigest: forge.pki.oids.messageDigest,
15
+ signingTime: forge.pki.oids.signingTime
16
+ };
17
+ const cnOf = (c) => String(c.subject.getField("CN")?.value ?? "");
18
+ const cnpjOfCn = (cn) => /:(\d{14})\s*$/.exec(cn)?.[1] ?? null;
19
+ /** Opens a PKCS#12 file (bytes) with its password; throws a readable error on a wrong password or an empty file. */
20
+ export function openPfx(pfx, password) {
21
+ let p12;
22
+ try {
23
+ p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(forge.util.binary.raw.encode(pfx)), false, password);
24
+ }
25
+ catch (e) {
26
+ throw new Error(`could not open the certificate file (wrong password or not a .pfx/.p12): ${e.message}`);
27
+ }
28
+ const keyBag = [OID.shroudedKeyBag, OID.keyBag].flatMap((t) => p12.getBags({ bagType: t })[t] ?? []).find((b) => b.key);
29
+ if (!keyBag?.key)
30
+ throw new Error("the file has no private key");
31
+ const key = keyBag.key;
32
+ const certs = (p12.getBags({ bagType: OID.certBag })[OID.certBag] ?? []).map((b) => b.cert).filter((c) => !!c);
33
+ // The signer is the certificate whose public key matches the private key; the others are its chain.
34
+ const leaf = certs.find((c) => c.publicKey.n?.equals(key.n));
35
+ if (!leaf)
36
+ throw new Error("no certificate in the file matches its private key");
37
+ return {
38
+ subject: leaf.subject.attributes.map((a) => `${a.shortName ?? a.name}=${a.value}`).join(", "),
39
+ cnpj: cnpjOfCn(cnOf(leaf)),
40
+ notAfter: leaf.validity.notAfter,
41
+ sign(text) {
42
+ const p7 = forge.pkcs7.createSignedData();
43
+ p7.content = forge.util.createBuffer(forge.util.encodeUtf8(text));
44
+ p7.addCertificate(leaf);
45
+ for (const c of certs)
46
+ if (c !== leaf)
47
+ p7.addCertificate(c);
48
+ p7.addSigner({
49
+ key,
50
+ certificate: leaf,
51
+ digestAlgorithm: OID.sha256,
52
+ authenticatedAttributes: [
53
+ { type: OID.contentType, value: OID.data },
54
+ { type: OID.messageDigest },
55
+ { type: OID.signingTime, value: new Date() }
56
+ ]
57
+ });
58
+ p7.sign({ detached: true });
59
+ return forge.util.encode64(forge.asn1.toDer(p7.toAsn1()).getBytes());
60
+ }
61
+ };
62
+ }
63
+ /** One call: sign `text` with the e-CNPJ in a .pfx/.p12 file. Returns the base64 DER the issuer expects as ecnpj_signature. */
64
+ export function signWithECnpjPfx(pfx, password, text) {
65
+ return openPfx(pfx, password).sign(text);
66
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kya-brasil/agent-tools",
3
- "version": "0.5.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 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.",
3
+ "version": "0.7.0",
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 .pfx, 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",
7
7
  "main": "./dist/index.js",
@@ -54,17 +54,22 @@
54
54
  "@x402/core": "^2.28.0",
55
55
  "@x402/evm": "^2.28.0",
56
56
  "@x402/fetch": "^2.28.0",
57
+ "node-forge": "1.4.0",
57
58
  "pkijs": "^3.4.1",
58
59
  "viem": "^2.57.5"
59
60
  },
60
61
  "devDependencies": {
61
- "@kya-br/core": "workspace:*",
62
+ "@kya-brasil/core": "workspace:*",
62
63
  "@peculiar/asn1-schema": "^2.3.15",
63
64
  "@peculiar/asn1-x509": "^2.3.15",
64
65
  "@peculiar/x509": "^2.1.0",
65
66
  "@types/node": "^22.20.5",
67
+ "@types/node-forge": "^1.3.14",
66
68
  "reflect-metadata": "^0.2.2",
67
69
  "typescript": "^5.9.3",
68
70
  "vitest": "^4.1.11"
71
+ },
72
+ "bin": {
73
+ "kya-sign": "./dist/cli.js"
69
74
  }
70
75
  }