@kya-brasil/agent-tools 0.8.0 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +65 -4
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -159,6 +159,10 @@ The PIN comes from `KYA_TOKEN_PIN` or is asked for. The key never leaves the tok
159
159
  software PKCS#11 token) in CI. The signing code is in `@kya-brasil/core` (`openPfx`, `ecnpjSigner`,
160
160
  `buildDetachedCms`), re-exported here; any other token or cloud signer plugs into `ecnpjSigner`.
161
161
 
162
+ **Cloud certificate (PSC).** `kya-sign --cloud ./my-psc.mjs --link "<sign_url>"` with a module that implements
163
+ `CloudSignatureProvider` for your PSC ([template](https://github.com/celfons/kya-brasil/tree/main/examples/cloud-provider));
164
+ the browser opens for the approval and the terminal receives the redirect on `127.0.0.1`.
165
+
162
166
  **From the agent's side**, `kya.signLink(credentialId, { challenge_id, agent_signature })` (free) returns the
163
167
  `sign_url` to send, the same as `signingLink(...)` offline and the MCP tool `kya_sign_link`.
164
168
 
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 { openPfx, readSigningLink } from "@kya-brasil/core";
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
- if (!pfxPath && !library) {
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 (library)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kya-brasil/agent-tools",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.8.0"
59
+ "@kya-brasil/core": "^0.9.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@peculiar/asn1-schema": "^2.3.15",