@kya-brasil/agent-tools 0.9.0 → 0.11.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 +33 -0
- package/dist/auth.d.ts +52 -5
- package/dist/auth.js +108 -27
- package/dist/index.d.ts +48 -7
- package/dist/index.js +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,6 +100,39 @@ 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
|
+
### Sub-agents (delegation, D-30): one certificate signature for the whole tree
|
|
104
|
+
|
|
105
|
+
When the mandate carries `delegation: { max_depth, max_ttl_seconds }`, the agent hands a narrower mandate to
|
|
106
|
+
another key with its own signature, no new e-CNPJ or e-CPF signature. The sub-agent signs its requests with its own
|
|
107
|
+
key and sends the chain; your middleware verifies it offline and `check()` / `allowed()` evaluate every mandate in it.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { signDelegation, signRequest } from "@kya-brasil/agent-tools";
|
|
111
|
+
const chain = await signDelegation({ credential, key: agentKey, delegate: { kind: "web-bot-auth", id: subPublicKey },
|
|
112
|
+
mandate: { actions: ["quote"], max_per_operation: { amount: 500, currency: "BRL" } }, ttlSeconds: 600 });
|
|
113
|
+
const headers = await signRequest({ credentialId, key: subKey, delegation: chain, method: "GET", url });
|
|
114
|
+
// server side: c.get("kya").delegation → { depth, delegate, mandates, expires_at, chain }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Short-lived (at most `max_ttl_seconds`, never past the credential), contained in the parent mandate, at most 3
|
|
118
|
+
levels; revoking the credential ends the whole tree. The issuer stores nothing. [SPEC](https://github.com/celfons/kya-brasil/blob/main/docs/SPEC.md) section 17.
|
|
119
|
+
|
|
120
|
+
## Person: an agent that acts for you (e-CPF)
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const reg = await kya.register({ agent: { kind: "evm", id: agentAddress }, principal: "person", mandate }); // $0.25
|
|
124
|
+
// agent signs reg.challenge.text; you sign it with your e-CPF (reg.sign_url, kya-sign or signWithECnpjPfx: same code)
|
|
125
|
+
const { credential } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature: ecpfSignature });
|
|
126
|
+
// credential.principal = { kind: "person", verified_by: "icp-brasil-e-cpf" }: nothing personal
|
|
127
|
+
|
|
128
|
+
// A verifier that already has the person's CPF:
|
|
129
|
+
const m = await kya.principalMatch(credentialId, "529.982.247-25"); // $0.01, signed: { match, subject_ref? }
|
|
130
|
+
|
|
131
|
+
// The person, to see their agents and every check made on them:
|
|
132
|
+
const text = personAgentsText(); // sign it with the e-CPF
|
|
133
|
+
const mine = await kya.personAgents({ signed_text: text, ecpf_signature });
|
|
134
|
+
```
|
|
135
|
+
|
|
103
136
|
## Company: give your own agent a credential
|
|
104
137
|
|
|
105
138
|
```ts
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Delegation, type DelegationAnchor } from "@kya-brasil/core";
|
|
2
|
+
import { type ActionQuery, type AgentKey, type Credential, type CredentialStatus, type KyaClient, type Mandate, type Verdict, type Verification, type KyaOptions } from "./index.js";
|
|
2
3
|
export declare const KYA_HEADERS: {
|
|
3
4
|
readonly credential: "kya-credential";
|
|
4
5
|
readonly created: "kya-created";
|
|
5
6
|
readonly expires: "kya-expires";
|
|
6
7
|
readonly signature: "kya-signature";
|
|
7
8
|
readonly content: "kya-content-sha256";
|
|
9
|
+
readonly delegation: "kya-delegation";
|
|
8
10
|
};
|
|
11
|
+
export type { Delegation } from "@kya-brasil/core";
|
|
9
12
|
export declare function sha256Hex(data: string | ArrayBuffer | Uint8Array): Promise<string>;
|
|
10
13
|
/** Canonical URL for the signature: origin (lowercase host) + path + query, no fragment. */
|
|
11
14
|
export declare function canonicalUrl(url: string): string;
|
|
@@ -16,6 +19,7 @@ export declare function requestText(p: {
|
|
|
16
19
|
created: number;
|
|
17
20
|
expires: number;
|
|
18
21
|
contentSha256?: string | null;
|
|
22
|
+
delegationSha256?: string | null;
|
|
19
23
|
}): string;
|
|
20
24
|
export type SigningKey = {
|
|
21
25
|
kind: "evm" | "erc8004";
|
|
@@ -24,7 +28,10 @@ export type SigningKey = {
|
|
|
24
28
|
kind: "web-bot-auth";
|
|
25
29
|
privateKey: CryptoKey | string;
|
|
26
30
|
};
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Signs a request with the agent key and returns the KYA headers to send with it. A sub-agent passes its
|
|
33
|
+
* `delegation` chain (from signDelegation) and signs with its own key.
|
|
34
|
+
*/
|
|
28
35
|
export declare function signRequest(p: {
|
|
29
36
|
credentialId: string;
|
|
30
37
|
key: SigningKey;
|
|
@@ -33,7 +40,28 @@ export declare function signRequest(p: {
|
|
|
33
40
|
body?: string | ArrayBuffer | Uint8Array | null;
|
|
34
41
|
ttlSeconds?: number;
|
|
35
42
|
now?: Date;
|
|
43
|
+
delegation?: Delegation[];
|
|
36
44
|
}): Promise<Record<string, string>>;
|
|
45
|
+
/** The key a signing key verifies as, in the form a credential or a delegation names it. */
|
|
46
|
+
export declare function agentKeyOf(key: SigningKey): Promise<AgentKey>;
|
|
47
|
+
/**
|
|
48
|
+
* Delegation to a sub-agent (D-30): hands `delegate` a mandate contained in the delegator's, for `ttlSeconds`
|
|
49
|
+
* (at most the mandate's `delegation.max_ttl_seconds`). `key` is the credential's agent key, or, to delegate
|
|
50
|
+
* again, the key of the last delegate in `chain`. Returns the new chain; the sub-agent sends it with
|
|
51
|
+
* signRequest({ delegation }). Nothing goes to the issuer.
|
|
52
|
+
*/
|
|
53
|
+
export declare function signDelegation(p: {
|
|
54
|
+
credential: DelegationAnchor | Credential;
|
|
55
|
+
key: SigningKey;
|
|
56
|
+
delegate: {
|
|
57
|
+
kind: "evm" | "web-bot-auth";
|
|
58
|
+
id: string;
|
|
59
|
+
};
|
|
60
|
+
mandate: Mandate;
|
|
61
|
+
ttlSeconds: number;
|
|
62
|
+
chain?: Delegation[];
|
|
63
|
+
now?: Date;
|
|
64
|
+
}): Promise<Delegation[]>;
|
|
37
65
|
export interface KyaAuthOptions extends KyaOptions {
|
|
38
66
|
/** How long a fetched credential is reused before asking the issuer again (ms). Default 60 000. */
|
|
39
67
|
cacheTtlMs?: number;
|
|
@@ -51,13 +79,28 @@ export interface KyaContext {
|
|
|
51
79
|
status: CredentialStatus;
|
|
52
80
|
mandate: Mandate;
|
|
53
81
|
credential: Verification["credential"];
|
|
54
|
-
/** Which key verified the request signature. */
|
|
82
|
+
/** Which key verified the request signature (the sub-agent's key on a delegated request). */
|
|
55
83
|
signer: string;
|
|
84
|
+
/**
|
|
85
|
+
* D-30: present when a sub-agent signed the request under a delegation chain. `agent` above stays the
|
|
86
|
+
* credential's agent; `delegate` is the sub-agent; every mandate in `mandates` must allow an action.
|
|
87
|
+
*/
|
|
88
|
+
delegation: {
|
|
89
|
+
depth: number;
|
|
90
|
+
delegate: AgentKey;
|
|
91
|
+
expires_at: string;
|
|
92
|
+
chain: Delegation[];
|
|
93
|
+
mandates: Mandate[];
|
|
94
|
+
} | null;
|
|
56
95
|
/** Whether the body hash was checked ("verified"), absent from the signature ("none") or not checkable ("skipped"). */
|
|
57
96
|
body: "verified" | "none" | "skipped";
|
|
58
|
-
/** Local evaluation of the mandate (no daily ceilings; those need the issuer). */
|
|
97
|
+
/** Local evaluation of the mandate (no daily ceilings; those need the issuer). On a delegated request, of every mandate in the chain. */
|
|
59
98
|
check(q: ActionQuery): LocalVerdict;
|
|
60
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Signed verdict from the issuer ($0.01, needs privateKey or internalKey in the options). The issuer evaluates
|
|
101
|
+
* the credential's mandate; on a delegated request the delegated mandates are evaluated here first, and when
|
|
102
|
+
* one refuses the answer is that local refusal (unsigned, `delegation_level` set, the issuer is not called).
|
|
103
|
+
*/
|
|
61
104
|
allowed(q: ActionQuery): Promise<Verdict>;
|
|
62
105
|
}
|
|
63
106
|
export type KyaAuthResult = {
|
|
@@ -84,6 +127,10 @@ export interface LocalVerdict {
|
|
|
84
127
|
* max_per_day when you pass spent_today (nobody else counts it: the issuer keeps no record of acts).
|
|
85
128
|
*/
|
|
86
129
|
export declare function checkMandate(mandate: Mandate, q: ActionQuery, now?: Date): LocalVerdict;
|
|
130
|
+
/** checkMandate over every mandate of a delegation chain (credential first): allowed only when all allow it. */
|
|
131
|
+
export declare function checkMandates(mandates: Mandate[], q: ActionQuery, now?: Date): LocalVerdict & {
|
|
132
|
+
levels: LocalVerdict[];
|
|
133
|
+
};
|
|
87
134
|
export interface KyaAuth {
|
|
88
135
|
client: KyaClient;
|
|
89
136
|
/** Authenticate any Web-standard Request. Never throws; read `ok`. */
|
package/dist/auth.js
CHANGED
|
@@ -16,9 +16,14 @@
|
|
|
16
16
|
* const headers = await signRequest({ credentialId, key: { kind: "evm", privateKey }, method: "POST", url, body });
|
|
17
17
|
* await fetch(url, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body });
|
|
18
18
|
*
|
|
19
|
-
*
|
|
19
|
+
* Sub-agent (D-30, the credential's mandate allows delegation):
|
|
20
|
+
* const chain = await signDelegation({ credential, key: parentKey, delegate: { kind: "evm", id: sub }, mandate: { actions: ["quote"] }, ttlSeconds: 600 });
|
|
21
|
+
* const headers = await signRequest({ credentialId, key: subKey, delegation: chain, method: "GET", url });
|
|
22
|
+
*
|
|
23
|
+
* Request text (what the agent signs; the profile is in docs/SPEC.md section 14):
|
|
20
24
|
* KYA Brasil: request
|
|
21
25
|
* credential: <credential_id>
|
|
26
|
+
* delegation: <sha256 hex of the KYA-Delegation header> (only on delegated requests)
|
|
22
27
|
* method: <METHOD>
|
|
23
28
|
* url: <origin + path + query, lowercase host>
|
|
24
29
|
* created: <unix seconds>
|
|
@@ -26,10 +31,11 @@
|
|
|
26
31
|
* content-sha256: <hex of the body | ->
|
|
27
32
|
*/
|
|
28
33
|
import { verifyMessage } from "viem";
|
|
34
|
+
import { encodeDelegationHeader, decodeDelegationHeader, prepareDelegation, verifyDelegationChain } from "@kya-brasil/core";
|
|
29
35
|
import { privateKeyToAccount } from "viem/accounts";
|
|
30
36
|
import { createKyaClient } from "./index.js";
|
|
31
37
|
import { fromBase64Url, toBase64Url } from "./base64url.js";
|
|
32
|
-
export const KYA_HEADERS = { credential: "kya-credential", created: "kya-created", expires: "kya-expires", signature: "kya-signature", content: "kya-content-sha256" };
|
|
38
|
+
export const KYA_HEADERS = { credential: "kya-credential", created: "kya-created", expires: "kya-expires", signature: "kya-signature", content: "kya-content-sha256", delegation: "kya-delegation" };
|
|
33
39
|
const enc = new TextEncoder();
|
|
34
40
|
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
35
41
|
const b64u = toBase64Url;
|
|
@@ -47,6 +53,7 @@ export function requestText(p) {
|
|
|
47
53
|
return [
|
|
48
54
|
"KYA Brasil: request",
|
|
49
55
|
`credential: ${p.credentialId}`,
|
|
56
|
+
...(p.delegationSha256 ? [`delegation: ${p.delegationSha256}`] : []),
|
|
50
57
|
`method: ${p.method.toUpperCase()}`,
|
|
51
58
|
`url: ${canonicalUrl(p.url)}`,
|
|
52
59
|
`created: ${p.created}`,
|
|
@@ -54,26 +61,57 @@ export function requestText(p) {
|
|
|
54
61
|
`content-sha256: ${p.contentSha256 ?? "-"}`
|
|
55
62
|
].join("\n");
|
|
56
63
|
}
|
|
57
|
-
|
|
64
|
+
// --- Agent side -------------------------------------------------------------------------------
|
|
65
|
+
const importPkcs8 = (pem, extractable = false) => crypto.subtle.importKey("pkcs8", unb64u(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")), { name: "Ed25519" }, extractable, ["sign"]);
|
|
66
|
+
async function signText(key, text) {
|
|
67
|
+
if (key.kind === "web-bot-auth") {
|
|
68
|
+
const k = typeof key.privateKey === "string" ? await importPkcs8(key.privateKey) : key.privateKey;
|
|
69
|
+
return b64u(new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, k, enc.encode(text))));
|
|
70
|
+
}
|
|
71
|
+
return privateKeyToAccount(key.privateKey).signMessage({ message: text });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Signs a request with the agent key and returns the KYA headers to send with it. A sub-agent passes its
|
|
75
|
+
* `delegation` chain (from signDelegation) and signs with its own key.
|
|
76
|
+
*/
|
|
58
77
|
export async function signRequest(p) {
|
|
59
78
|
const created = Math.floor((p.now ?? new Date()).getTime() / 1000);
|
|
60
79
|
const expires = created + (p.ttlSeconds ?? 60);
|
|
61
80
|
const contentSha256 = p.body !== undefined && p.body !== null && (typeof p.body !== "string" || p.body.length > 0) ? await sha256Hex(p.body) : null;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
signature = b64u(new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, enc.encode(text))));
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
signature = await privateKeyToAccount(p.key.privateKey).signMessage({ message: text });
|
|
70
|
-
}
|
|
81
|
+
const delegationHeader = p.delegation && p.delegation.length ? encodeDelegationHeader(p.delegation) : null;
|
|
82
|
+
const delegationSha256 = delegationHeader ? await sha256Hex(delegationHeader) : null;
|
|
83
|
+
const text = requestText({ credentialId: p.credentialId, method: p.method, url: p.url, created, expires, contentSha256, delegationSha256 });
|
|
84
|
+
const signature = await signText(p.key, text);
|
|
71
85
|
const headers = { [KYA_HEADERS.credential]: p.credentialId, [KYA_HEADERS.created]: String(created), [KYA_HEADERS.expires]: String(expires), [KYA_HEADERS.signature]: signature };
|
|
72
86
|
if (contentSha256)
|
|
73
87
|
headers[KYA_HEADERS.content] = contentSha256;
|
|
88
|
+
if (delegationHeader)
|
|
89
|
+
headers[KYA_HEADERS.delegation] = delegationHeader;
|
|
74
90
|
return headers;
|
|
75
91
|
}
|
|
76
|
-
|
|
92
|
+
/** The key a signing key verifies as, in the form a credential or a delegation names it. */
|
|
93
|
+
export async function agentKeyOf(key) {
|
|
94
|
+
if (key.kind !== "web-bot-auth")
|
|
95
|
+
return { kind: "evm", id: privateKeyToAccount(key.privateKey).address };
|
|
96
|
+
const k = typeof key.privateKey === "string" ? await importPkcs8(key.privateKey, true) : key.privateKey;
|
|
97
|
+
const jwk = (await crypto.subtle.exportKey("jwk", k));
|
|
98
|
+
if (!jwk.x)
|
|
99
|
+
throw new Error("cannot derive the Ed25519 public key from this private key");
|
|
100
|
+
return { kind: "web-bot-auth", id: jwk.x };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Delegation to a sub-agent (D-30): hands `delegate` a mandate contained in the delegator's, for `ttlSeconds`
|
|
104
|
+
* (at most the mandate's `delegation.max_ttl_seconds`). `key` is the credential's agent key, or, to delegate
|
|
105
|
+
* again, the key of the last delegate in `chain`. Returns the new chain; the sub-agent sends it with
|
|
106
|
+
* signRequest({ delegation }). Nothing goes to the issuer.
|
|
107
|
+
*/
|
|
108
|
+
export async function signDelegation(p) {
|
|
109
|
+
const chain = p.chain ?? [];
|
|
110
|
+
const { delegation, text } = await prepareDelegation({ credential: p.credential, chain, delegate: p.delegate, mandate: p.mandate, ttlSeconds: p.ttlSeconds, now: p.now });
|
|
111
|
+
const signature = await signText(p.key, text);
|
|
112
|
+
return [...chain, { ...delegation, signature }];
|
|
113
|
+
}
|
|
114
|
+
const RESERVED = ["actions", "max_per_operation", "max_per_day", "currencies", "counterparties", "payment_instruments", "valid_from", "valid_until", "delegation"];
|
|
77
115
|
function normalizeCounterparty(c) {
|
|
78
116
|
const t = c.trim();
|
|
79
117
|
const digits = t.replace(/\D/g, "");
|
|
@@ -133,6 +171,17 @@ export function checkMandate(mandate, q, now = new Date()) {
|
|
|
133
171
|
not_evaluated.push("max_per_day (pass spent_today: the issuer keeps no record of acts)");
|
|
134
172
|
return { allowed: checks.every((c) => c.ok), checks, not_evaluated };
|
|
135
173
|
}
|
|
174
|
+
/** checkMandate over every mandate of a delegation chain (credential first): allowed only when all allow it. */
|
|
175
|
+
export function checkMandates(mandates, q, now = new Date()) {
|
|
176
|
+
const levels = mandates.map((m) => checkMandate(m, q, now));
|
|
177
|
+
const prefix = (i, s) => (i === 0 ? s : `delegation ${i}: ${s}`);
|
|
178
|
+
return {
|
|
179
|
+
allowed: levels.length > 0 && levels.every((l) => l.allowed),
|
|
180
|
+
checks: levels.flatMap((l, i) => l.checks.map((c) => ({ ...c, rule: prefix(i, c.rule) }))),
|
|
181
|
+
not_evaluated: [...new Set(levels.flatMap((l, i) => l.not_evaluated.map((n) => prefix(i, n))))],
|
|
182
|
+
levels
|
|
183
|
+
};
|
|
184
|
+
}
|
|
136
185
|
const headerOf = (headers, name) => {
|
|
137
186
|
if (headers instanceof Headers)
|
|
138
187
|
return headers.get(name);
|
|
@@ -177,6 +226,7 @@ export function createKyaAuth(opts = {}) {
|
|
|
177
226
|
const expires = Number(headerOf(p.headers, KYA_HEADERS.expires));
|
|
178
227
|
const signature = headerOf(p.headers, KYA_HEADERS.signature);
|
|
179
228
|
const contentHeader = headerOf(p.headers, KYA_HEADERS.content);
|
|
229
|
+
const delegationHeader = headerOf(p.headers, KYA_HEADERS.delegation);
|
|
180
230
|
if (!credentialId || !signature || !Number.isFinite(created) || !Number.isFinite(expires))
|
|
181
231
|
return fail(401, "KYA_HEADERS_MISSING", `send ${KYA_HEADERS.credential}, ${KYA_HEADERS.created}, ${KYA_HEADERS.expires} and ${KYA_HEADERS.signature}`);
|
|
182
232
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -208,16 +258,43 @@ export function createKyaAuth(opts = {}) {
|
|
|
208
258
|
const status = e.status;
|
|
209
259
|
return status === 404 ? fail(401, "KYA_CREDENTIAL_UNKNOWN", "unknown credential_id") : fail(502, "KYA_ISSUER_UNAVAILABLE", e.message);
|
|
210
260
|
}
|
|
211
|
-
|
|
212
|
-
|
|
261
|
+
let delegation = null;
|
|
262
|
+
if (delegationHeader) {
|
|
263
|
+
const d = await delegationOf(credentialId, v, delegationHeader);
|
|
264
|
+
if (!d.ok)
|
|
265
|
+
return d.result;
|
|
266
|
+
delegation = d.delegation;
|
|
267
|
+
}
|
|
268
|
+
const delegationSha256 = delegationHeader ? await sha256Hex(delegationHeader) : null;
|
|
269
|
+
const text = requestText({ credentialId, method: p.method, url: p.url, created, expires, contentSha256: contentHeader, delegationSha256 });
|
|
270
|
+
return finish(credentialId, v, text, signature, bodyState, delegation);
|
|
271
|
+
}
|
|
272
|
+
/** D-30: verifies a KYA-Delegation chain against the credential (offline, core rules). */
|
|
273
|
+
async function delegationOf(credentialId, v, header) {
|
|
274
|
+
const no = (status, error, message) => ({ ok: false, result: fail(status, error, message) });
|
|
275
|
+
if (!v.credential)
|
|
276
|
+
return no(403, "KYA_CREDENTIAL_NOT_ACTIVE", `credential is ${v.status}`);
|
|
277
|
+
let chain;
|
|
278
|
+
try {
|
|
279
|
+
chain = decodeDelegationHeader(header);
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
return no(401, "KYA_DELEGATION_MALFORMED", e.message);
|
|
283
|
+
}
|
|
284
|
+
const anchor = { credential_id: credentialId, agent: v.agent, mandate: v.credential.mandate, expires_at: v.credential.expires_at };
|
|
285
|
+
const r = await verifyDelegationChain({ credential: anchor, chain });
|
|
286
|
+
if (!r.ok)
|
|
287
|
+
return no(r.error === "KYA_DELEGATION_NOT_ALLOWED" || r.error === "KYA_DELEGATION_BROADER" ? 403 : 401, r.error, r.message);
|
|
288
|
+
return { ok: true, delegation: { depth: r.depth, delegate: r.delegate, expires_at: r.expires_at, chain: chain, mandates: r.mandates } };
|
|
213
289
|
}
|
|
214
|
-
async function finish(credentialId, v, text, signature, bodyState) {
|
|
215
|
-
const sig = await verifySignature(v.agent, text, signature);
|
|
290
|
+
async function finish(credentialId, v, text, signature, bodyState, delegation = null) {
|
|
291
|
+
const sig = await verifySignature(delegation ? delegation.delegate : v.agent, text, signature);
|
|
216
292
|
if (!sig.ok)
|
|
217
|
-
return fail(401, "KYA_BAD_SIGNATURE", "signature does not match the credential's agent key over the signed text");
|
|
293
|
+
return fail(401, "KYA_BAD_SIGNATURE", delegation ? "signature does not match the delegate's key over the signed text" : "signature does not match the credential's agent key over the signed text");
|
|
218
294
|
if (v.status !== "active")
|
|
219
295
|
return fail(403, "KYA_CREDENTIAL_NOT_ACTIVE", `credential is ${v.status}${v.status_reason ? `: ${v.status_reason}` : ""}`);
|
|
220
296
|
const mandate = v.credential?.mandate ?? {};
|
|
297
|
+
const mandates = delegation ? delegation.mandates : [mandate];
|
|
221
298
|
const ctx = {
|
|
222
299
|
credential_id: credentialId,
|
|
223
300
|
agent: v.agent,
|
|
@@ -227,8 +304,19 @@ export function createKyaAuth(opts = {}) {
|
|
|
227
304
|
credential: v.credential,
|
|
228
305
|
signer: sig.signer,
|
|
229
306
|
body: bodyState,
|
|
230
|
-
|
|
231
|
-
|
|
307
|
+
delegation,
|
|
308
|
+
check: (q) => (delegation ? checkMandates(mandates, q) : checkMandate(mandate, q)),
|
|
309
|
+
allowed: async (q) => {
|
|
310
|
+
if (delegation) {
|
|
311
|
+
for (let i = 1; i < mandates.length; i++) {
|
|
312
|
+
const local = checkMandate(mandates[i], q);
|
|
313
|
+
if (!local.allowed) {
|
|
314
|
+
return { credential_id: credentialId, status: v.status, allowed: false, reason: `outside the mandate of delegation ${i}`, checks: local.checks.map((c) => ({ field: c.rule, ok: c.ok, detail: c.detail })), not_evaluated: local.not_evaluated, delegation_level: i };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return client.allowed(credentialId, q);
|
|
319
|
+
}
|
|
232
320
|
};
|
|
233
321
|
return { ok: true, kya: ctx };
|
|
234
322
|
}
|
|
@@ -309,13 +397,6 @@ export const KYA_REPLY_HEADER = "kya-in-reply-to";
|
|
|
309
397
|
export function responseText(p) {
|
|
310
398
|
return ["KYA Brasil: response", `credential: ${p.credentialId}`, `in-reply-to: ${p.inReplyTo}`, `created: ${p.created}`, `expires: ${p.expires}`, `content-sha256: ${p.contentSha256 ?? "-"}`].join("\n");
|
|
311
399
|
}
|
|
312
|
-
async function signText(key, text) {
|
|
313
|
-
if (key.kind === "web-bot-auth") {
|
|
314
|
-
const k = typeof key.privateKey === "string" ? await crypto.subtle.importKey("pkcs8", unb64u(key.privateKey.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")), { name: "Ed25519" }, false, ["sign"]) : key.privateKey;
|
|
315
|
-
return b64u(new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, k, enc.encode(text))));
|
|
316
|
-
}
|
|
317
|
-
return privateKeyToAccount(key.privateKey).signMessage({ message: text });
|
|
318
|
-
}
|
|
319
400
|
/** Callee side: headers that prove who answered, bound to the request signature it replies to. */
|
|
320
401
|
export async function signResponse(p) {
|
|
321
402
|
const created = Math.floor((p.now ?? new Date()).getTime() / 1000);
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface Mandate {
|
|
|
33
33
|
ref_sha256: string;
|
|
34
34
|
label?: string;
|
|
35
35
|
}[];
|
|
36
|
+
/** D-30: the agent may hand a narrower mandate to sub-agents (signDelegation), up to max_depth levels below it. */
|
|
37
|
+
delegation?: {
|
|
38
|
+
max_depth: number;
|
|
39
|
+
max_ttl_seconds: number;
|
|
40
|
+
};
|
|
36
41
|
valid_from?: string;
|
|
37
42
|
valid_until?: string;
|
|
38
43
|
[k: string]: unknown;
|
|
@@ -49,10 +54,13 @@ export interface Credential {
|
|
|
49
54
|
credential_id: string;
|
|
50
55
|
version: number;
|
|
51
56
|
agent: AgentKey;
|
|
57
|
+
/** A company, or (D-29) a person: { kind: "person", verified_by: "icp-brasil-e-cpf" } with nothing personal. */
|
|
52
58
|
principal: {
|
|
53
|
-
|
|
59
|
+
kind?: "company" | "person";
|
|
60
|
+
cnpj?: string;
|
|
54
61
|
legal_name?: string;
|
|
55
62
|
registration_status?: string;
|
|
63
|
+
verified_by?: string;
|
|
56
64
|
};
|
|
57
65
|
authorized_by: {
|
|
58
66
|
method: string;
|
|
@@ -79,9 +87,11 @@ export interface Verification {
|
|
|
79
87
|
status_reason: string | null;
|
|
80
88
|
agent: AgentKey;
|
|
81
89
|
principal: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
kind?: "person";
|
|
91
|
+
verified_by?: string;
|
|
92
|
+
cnpj?: string;
|
|
93
|
+
cnpj_formatted?: string;
|
|
94
|
+
legal_name?: string | null;
|
|
85
95
|
};
|
|
86
96
|
credential: Credential | null;
|
|
87
97
|
events: {
|
|
@@ -127,8 +137,9 @@ export interface Registration {
|
|
|
127
137
|
credential_id: string;
|
|
128
138
|
agent: AgentKey;
|
|
129
139
|
principal: {
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
kind?: "person";
|
|
141
|
+
cnpj?: string;
|
|
142
|
+
cnpj_formatted?: string;
|
|
132
143
|
};
|
|
133
144
|
mandate: Mandate;
|
|
134
145
|
challenge: {
|
|
@@ -195,11 +206,39 @@ export interface KyaClient {
|
|
|
195
206
|
/** $0.01: signed verdict, is this action inside the mandate and is the credential active? */
|
|
196
207
|
allowed(credentialId: string, q: ActionQuery): Promise<Verdict>;
|
|
197
208
|
/** $0.25: register an agent for a company; returns the challenge both parties sign. */
|
|
209
|
+
/** $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
210
|
register(input: {
|
|
199
211
|
agent: AgentKey;
|
|
200
|
-
cnpj
|
|
212
|
+
cnpj?: string;
|
|
213
|
+
principal?: "company" | "person";
|
|
201
214
|
mandate: Mandate;
|
|
202
215
|
}): Promise<Registration>;
|
|
216
|
+
/** $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. */
|
|
217
|
+
principalMatch(credentialId: string, cpf: string): Promise<{
|
|
218
|
+
credential_id: string;
|
|
219
|
+
match: boolean;
|
|
220
|
+
subject_ref?: string;
|
|
221
|
+
verifier: string;
|
|
222
|
+
checked_at: string;
|
|
223
|
+
attestation?: Attestation;
|
|
224
|
+
}>;
|
|
225
|
+
/** Free (D-29): the person's own credentials and the CPF checks made on each; sign personAgentsText() with the e-CPF. */
|
|
226
|
+
personAgents(input: {
|
|
227
|
+
signed_text: string;
|
|
228
|
+
ecpf_signature: string;
|
|
229
|
+
}): Promise<{
|
|
230
|
+
agents: {
|
|
231
|
+
credential_id: string;
|
|
232
|
+
agent: AgentKey;
|
|
233
|
+
status: string;
|
|
234
|
+
expires_at: string | null;
|
|
235
|
+
checks: {
|
|
236
|
+
verifier: string;
|
|
237
|
+
matched: boolean;
|
|
238
|
+
at: string;
|
|
239
|
+
}[];
|
|
240
|
+
}[];
|
|
241
|
+
}>;
|
|
203
242
|
/** 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
243
|
prove(credentialId: string, proof: Proof): Promise<{
|
|
205
244
|
credential: Credential; /** D-24: e.g. registration status not confirmed (issued anyway). */
|
|
@@ -373,6 +412,8 @@ export declare function verifyCredentialOffline(credential: Credential, opts?: {
|
|
|
373
412
|
now?: Date;
|
|
374
413
|
}): Promise<OfflineCheck>;
|
|
375
414
|
export * from "./auth.js";
|
|
415
|
+
export { isValidCpf, personAgentsText } from "@kya-brasil/core";
|
|
416
|
+
export { checkDelegatedAction, decodeDelegationHeader, encodeDelegationHeader, isMandateSubset, mandateSubsetProblems, verifyDelegationChain } from "@kya-brasil/core";
|
|
376
417
|
export { buildDetachedCms, cnpjFromCommonName, ecnpjSigner, openPfx, readSigningLink, signWithECnpjPfx, type ECnpjSigner, type RawSigner } from "@kya-brasil/core";
|
|
377
418
|
export { listTokenCertificates, openToken, type TokenCertificate, type TokenOptions } from "./pkcs11.js";
|
|
378
419
|
/**
|
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,9 @@ 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";
|
|
258
|
+
// Delegation to sub-agents (D-30): chain rules live in the core, signing and the middleware in ./auth.
|
|
259
|
+
export { checkDelegatedAction, decodeDelegationHeader, encodeDelegationHeader, isMandateSubset, mandateSubsetProblems, verifyDelegationChain } from "@kya-brasil/core";
|
|
255
260
|
export { buildDetachedCms, cnpjFromCommonName, ecnpjSigner, openPfx, readSigningLink, signWithECnpjPfx } from "@kya-brasil/core";
|
|
256
261
|
export { listTokenCertificates, openToken } from "./pkcs11.js";
|
|
257
262
|
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.11.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.11.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@peculiar/asn1-schema": "^2.3.15",
|