kxco-pq-agent 1.0.6 → 1.0.7

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 (2) hide show
  1. package/package.json +62 -62
  2. package/src/agent-identity.js +299 -299
package/package.json CHANGED
@@ -1,66 +1,66 @@
1
1
  {
2
- "name": "kxco-pq-agent",
3
- "version": "1.0.6",
4
- "description": "Post-quantum identity for AI agents and autonomous systems: a KYC-verified institution sponsors an ML-DSA-65 keypair and locked capability scope for any agent that cannot pass KYC itself.",
5
- "keywords": [
6
- "post-quantum",
7
- "pqc",
8
- "ml-dsa",
9
- "nist",
10
- "fips-204",
11
- "kxco",
12
- "agent-identity",
13
- "robot-identity",
14
- "machine-identity",
15
- "ai-agent",
16
- "capability-scope",
17
- "armature",
18
- "relay",
19
- "blockchain"
20
- ],
21
- "license": "Apache-2.0",
22
- "author": "Shayne Heffernan and John Heffernan",
23
- "contributors": [
24
- {
25
- "name": "Shayne Heffernan"
26
- },
27
- {
28
- "name": "John Heffernan"
29
- }
30
- ],
31
- "homepage": "https://kxco.ai",
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/KnightsbridgeAIQ/kxco-pq-agent.git"
2
+ "name": "kxco-pq-agent",
3
+ "version": "1.0.7",
4
+ "description": "Post-quantum identity for AI agents and autonomous systems: a KYC-verified institution sponsors an ML-DSA-65 keypair and locked capability scope for any agent that cannot pass KYC itself.",
5
+ "keywords": [
6
+ "post-quantum",
7
+ "pqc",
8
+ "ml-dsa",
9
+ "nist",
10
+ "fips-204",
11
+ "kxco",
12
+ "agent-identity",
13
+ "robot-identity",
14
+ "machine-identity",
15
+ "ai-agent",
16
+ "capability-scope",
17
+ "armature",
18
+ "relay",
19
+ "blockchain"
20
+ ],
21
+ "license": "Apache-2.0",
22
+ "author": "Shayne Heffernan and John Heffernan",
23
+ "contributors": [
24
+ {
25
+ "name": "Shayne Heffernan"
35
26
  },
36
- "bugs": {
37
- "url": "https://github.com/KnightsbridgeAIQ/kxco-pq-agent/issues"
38
- },
39
- "type": "module",
40
- "sideEffects": false,
41
- "main": "./src/index.js",
42
- "types": "./src/index.d.ts",
43
- "exports": {
44
- ".": {
45
- "types": "./src/index.d.ts",
46
- "import": "./src/index.js"
47
- }
48
- },
49
- "files": [
50
- "src",
51
- "LICENSE"
52
- ],
53
- "engines": {
54
- "node": ">=20.19"
55
- },
56
- "dependencies": {
57
- "kxco-post-quantum": "^1.3.0"
58
- },
59
- "scripts": {
60
- "test": "node --test --test-timeout=30000 test/agent.test.js"
61
- },
62
- "funding": "https://kxco.ai",
63
- "publishConfig": {
64
- "access": "public"
27
+ {
28
+ "name": "John Heffernan"
29
+ }
30
+ ],
31
+ "homepage": "https://kxco.ai",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/KnightsbridgeAIQ/kxco-pq-agent.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/KnightsbridgeAIQ/kxco-pq-agent/issues"
38
+ },
39
+ "type": "module",
40
+ "sideEffects": false,
41
+ "main": "./src/index.js",
42
+ "types": "./src/index.d.ts",
43
+ "exports": {
44
+ ".": {
45
+ "types": "./src/index.d.ts",
46
+ "import": "./src/index.js"
65
47
  }
48
+ },
49
+ "files": [
50
+ "src",
51
+ "LICENSE"
52
+ ],
53
+ "engines": {
54
+ "node": ">=20.19"
55
+ },
56
+ "dependencies": {
57
+ "kxco-post-quantum": "^1.3.0"
58
+ },
59
+ "scripts": {
60
+ "test": "node --test --test-timeout=30000 test/agent.test.js"
61
+ },
62
+ "funding": "https://kxco.ai",
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
66
  }
@@ -1,299 +1,299 @@
1
- import { mlDsa, fingerprint } from 'kxco-post-quantum'
2
- import { validateScope, hashScope } from './scope.js'
3
- import { canonicalize } from './jcs.js'
4
- import { KxcoPqAgentError } from './errors.js'
5
- import { AgentChainClient } from './agent-client.js'
6
-
7
- const CREDENTIAL_VERSION = '1'
8
- const IDENTITY_VERSION = '1'
9
-
10
- const enc = new TextEncoder()
11
-
12
- function b64url(bytes) {
13
- return Buffer.from(bytes).toString('base64url')
14
- }
15
-
16
- function fromB64url(s) {
17
- return new Uint8Array(Buffer.from(s, 'base64url'))
18
- }
19
-
20
- function parseDuration(val) {
21
- if (typeof val === 'number') return val * 1000
22
- const m = String(val).match(/^(\d+)(d|h|m|s|y)$/)
23
- if (!m) throw new KxcoPqAgentError(`invalid expiresIn '${val}' — use '30d', '1y', or seconds as a number`)
24
- const n = parseInt(m[1], 10)
25
- const ms = { d: 86400000, h: 3600000, m: 60000, s: 1000, y: 31536000000 }
26
- return n * ms[m[2]]
27
- }
28
-
29
- function credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt }) {
30
- return enc.encode([
31
- 'kxco-agent-credential-v1',
32
- agentKid,
33
- agentPublicKey,
34
- sponsorKid,
35
- agentType,
36
- label,
37
- model ?? '',
38
- canonicalize(scope),
39
- issuedAt,
40
- expiresAt,
41
- ].join('\n'))
42
- }
43
-
44
- const VALID_AGENT_TYPES = new Set(['llm', 'robot', 'iot', 'process'])
45
-
46
- export class KxcoAgentIdentity {
47
- #kid
48
- #keypair
49
- #sponsorKid
50
- #agentType
51
- #label
52
- #model
53
- #scope
54
- #issuedAt
55
- #expiresAt
56
- #credential
57
-
58
- constructor(opts) {
59
- this.#kid = opts.kid
60
- this.#keypair = opts.keypair
61
- this.#sponsorKid = opts.sponsorKid
62
- this.#agentType = opts.agentType
63
- this.#label = opts.label
64
- this.#model = opts.model ?? null
65
- this.#scope = opts.scope
66
- this.#issuedAt = opts.issuedAt
67
- this.#expiresAt = opts.expiresAt
68
- this.#credential = opts.credential
69
- }
70
-
71
- get kid() { return this.#kid }
72
- get sponsorKid() { return this.#sponsorKid }
73
- get agentType() { return this.#agentType }
74
- get label() { return this.#label }
75
- get model() { return this.#model }
76
- get scope() { return JSON.parse(JSON.stringify(this.#scope)) }
77
- get issuedAt() { return this.#issuedAt }
78
- get expiresAt() { return this.#expiresAt }
79
- get credential() { return JSON.parse(JSON.stringify(this.#credential)) }
80
-
81
- // ── Factory ───────────────────────────────────────────────────────────────
82
-
83
- /**
84
- * Create a new agent identity. Any KxcoIdentity holder may sponsor an agent.
85
- *
86
- * @param {object} opts
87
- * @param {{ kid: string, sign(msg: Uint8Array): Promise<Uint8Array> }} opts.sponsor
88
- * @param {string} opts.label — human-readable name for this agent
89
- * @param {'llm'|'robot'|'iot'|'process'} opts.agentType
90
- * @param {string} [opts.model] — model/hardware identifier (optional)
91
- * @param {object} opts.scope — locked capability manifest (see scope.js)
92
- * @param {string|number} opts.expiresIn — '30d', '1y', or seconds as number (mandatory)
93
- * @param {object} [opts.chain] — KxcoChain instance for on-chain registration
94
- */
95
- static async create({ sponsor, label, agentType, model, scope, expiresIn, chain } = {}) {
96
- if (!sponsor?.kid || typeof sponsor.sign !== 'function') {
97
- throw new KxcoPqAgentError('create: sponsor must have .kid and .sign(message)')
98
- }
99
- if (!label) throw new KxcoPqAgentError('create: label is required')
100
- if (!agentType) throw new KxcoPqAgentError('create: agentType is required')
101
- if (!VALID_AGENT_TYPES.has(agentType)) {
102
- throw new KxcoPqAgentError(`create: agentType must be one of: ${[...VALID_AGENT_TYPES].join(', ')}`)
103
- }
104
- if (!scope) throw new KxcoPqAgentError('create: scope is required')
105
- if (expiresIn == null) throw new KxcoPqAgentError('create: expiresIn is required — agents must have an expiry')
106
-
107
- validateScope(scope)
108
-
109
- const keypair = mlDsa.ml_dsa65.keygen()
110
- const agentKid = fingerprint(keypair.publicKey)
111
- const agentPubB64 = b64url(keypair.publicKey)
112
- const issuedAt = new Date().toISOString()
113
- const expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString()
114
-
115
- const sigMsg = credentialSigningMsg({
116
- agentKid,
117
- agentPublicKey: agentPubB64,
118
- sponsorKid: sponsor.kid,
119
- agentType,
120
- label,
121
- model,
122
- scope,
123
- issuedAt,
124
- expiresAt,
125
- })
126
-
127
- const sigBytes = await sponsor.sign(sigMsg)
128
- const credential = {
129
- 'kxco-agent': CREDENTIAL_VERSION,
130
- agentKid,
131
- agentPublicKey: agentPubB64,
132
- sponsorKid: sponsor.kid,
133
- agentType,
134
- label,
135
- ...(model && { model }),
136
- scope,
137
- issuedAt,
138
- expiresAt,
139
- sponsorSignature: b64url(sigBytes),
140
- }
141
-
142
- if (chain) {
143
- const scopeHash = await hashScope(scope)
144
- const expiresAtSec = Math.floor(new Date(expiresAt).getTime() / 1000)
145
- await chain.issueAgentCredential({
146
- agentKid,
147
- agentPublicKeyHex: Buffer.from(keypair.publicKey).toString('hex'),
148
- agentType,
149
- scopeHash,
150
- expiresAt: expiresAtSec,
151
- })
152
- }
153
-
154
- return new KxcoAgentIdentity({
155
- kid: agentKid,
156
- keypair,
157
- sponsorKid: sponsor.kid,
158
- agentType,
159
- label,
160
- model: model ?? null,
161
- scope,
162
- issuedAt,
163
- expiresAt,
164
- credential,
165
- })
166
- }
167
-
168
- // ── Signing ───────────────────────────────────────────────────────────────
169
-
170
- async sign(message) {
171
- if (!this.#keypair?.secretKey) {
172
- throw new KxcoPqAgentError('no signing key — reconstruct with KxcoAgentIdentity.import()')
173
- }
174
- return Buffer.from(mlDsa.sign(
175
- new Uint8Array(this.#keypair.secretKey),
176
- new Uint8Array(message),
177
- ), 'hex')
178
- }
179
-
180
- async getPublicKey() {
181
- return this.#keypair.publicKey
182
- }
183
-
184
- // ── Export / import ───────────────────────────────────────────────────────
185
-
186
- export() {
187
- return {
188
- 'kxco-agent-identity': IDENTITY_VERSION,
189
- kid: this.#kid,
190
- sponsorKid: this.#sponsorKid,
191
- agentType: this.#agentType,
192
- label: this.#label,
193
- ...(this.#model && { model: this.#model }),
194
- scope: this.#scope,
195
- issuedAt: this.#issuedAt,
196
- expiresAt: this.#expiresAt,
197
- secretKey: b64url(this.#keypair.secretKey),
198
- publicKey: b64url(this.#keypair.publicKey),
199
- credential: this.#credential,
200
- }
201
- }
202
-
203
- static async import(exported) {
204
- if (!exported || exported['kxco-agent-identity'] !== IDENTITY_VERSION) {
205
- throw new KxcoPqAgentError('import: invalid or unsupported agent identity format')
206
- }
207
- return new KxcoAgentIdentity({
208
- kid: exported.kid,
209
- keypair: { secretKey: fromB64url(exported.secretKey), publicKey: fromB64url(exported.publicKey) },
210
- sponsorKid: exported.sponsorKid,
211
- agentType: exported.agentType,
212
- label: exported.label,
213
- model: exported.model ?? null,
214
- scope: exported.scope,
215
- issuedAt: exported.issuedAt,
216
- expiresAt: exported.expiresAt,
217
- credential: exported.credential,
218
- })
219
- }
220
-
221
- // ── Chain client ──────────────────────────────────────────────────────────
222
-
223
- /**
224
- * Returns an AgentChainClient that sends agent-signed intents to the relay.
225
- * Each request automatically includes this agent's credential and kid.
226
- * @param {string} relay — relay base URL, e.g. 'https://relay.kxco.ai'
227
- * @param {{ timeout?: number }} [opts]
228
- */
229
- toChainClient(relay, { timeout } = {}) {
230
- return new AgentChainClient({ relay, agent: this, ...(timeout && { timeout }) })
231
- }
232
-
233
- // ── Static: verify a credential ──────────────────────────────────────────
234
-
235
- /**
236
- * Verify an agent credential envelope.
237
- * Pass sponsorPublicKey (Uint8Array) to perform full ML-DSA-65 signature verification.
238
- * Without it, only expiry and format are checked.
239
- *
240
- * @param {object} credential
241
- * @param {{ sponsorPublicKey?: Uint8Array }} [opts]
242
- */
243
- static async verify(credential, { sponsorPublicKey } = {}) {
244
- if (!credential || credential['kxco-agent'] !== CREDENTIAL_VERSION) {
245
- return { valid: false, error: 'invalid or unsupported credential format' }
246
- }
247
-
248
- const {
249
- agentKid, agentPublicKey, sponsorKid, agentType,
250
- label, model, scope, issuedAt, expiresAt, sponsorSignature,
251
- } = credential
252
-
253
- if (!agentKid || !agentPublicKey || !sponsorKid || !agentType || !issuedAt || !expiresAt || !sponsorSignature) {
254
- return { valid: false, error: 'malformed credential — missing required fields' }
255
- }
256
-
257
- if (new Date(expiresAt) < new Date()) {
258
- return { valid: false, error: 'agent credential has expired' }
259
- }
260
-
261
- if (sponsorPublicKey) {
262
- const msg = credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt })
263
- let ok
264
- try {
265
- ok = mlDsa.verify(new Uint8Array(sponsorPublicKey), msg, Buffer.from(fromB64url(sponsorSignature)).toString('hex'))
266
- } catch {
267
- ok = false
268
- }
269
- if (!ok) return { valid: false, error: 'sponsor signature invalid' }
270
- }
271
-
272
- return {
273
- valid: true,
274
- agentKid,
275
- sponsorKid,
276
- agentType,
277
- label,
278
- ...(model && { model }),
279
- scope,
280
- issuedAt,
281
- expiresAt,
282
- }
283
- }
284
-
285
- // ── Static: revoke on-chain ───────────────────────────────────────────────
286
-
287
- /**
288
- * Revoke an agent credential on-chain.
289
- * The chain parameter must be a KxcoChain instance belonging to the sponsor.
290
- *
291
- * @param {string} agentKid
292
- * @param {{ chain: object, reason?: string }} opts
293
- */
294
- static async revoke(agentKid, { chain, reason = '' } = {}) {
295
- if (!agentKid) throw new KxcoPqAgentError('revoke: agentKid is required')
296
- if (!chain) throw new KxcoPqAgentError('revoke: chain is required for on-chain revocation')
297
- return chain.revokeAgentCredential({ agentKid, reason })
298
- }
299
- }
1
+ import { mlDsa, fingerprint } from 'kxco-post-quantum'
2
+ import { validateScope, hashScope } from './scope.js'
3
+ import { canonicalize } from './jcs.js'
4
+ import { KxcoPqAgentError } from './errors.js'
5
+ import { AgentChainClient } from './agent-client.js'
6
+
7
+ const CREDENTIAL_VERSION = '1'
8
+ const IDENTITY_VERSION = '1'
9
+
10
+ const enc = new TextEncoder()
11
+
12
+ function b64url(bytes) {
13
+ return Buffer.from(bytes).toString('base64url')
14
+ }
15
+
16
+ function fromB64url(s) {
17
+ return new Uint8Array(Buffer.from(s, 'base64url'))
18
+ }
19
+
20
+ function parseDuration(val) {
21
+ if (typeof val === 'number') return val * 1000
22
+ const m = String(val).match(/^(\d+)(d|h|m|s|y)$/)
23
+ if (!m) throw new KxcoPqAgentError(`invalid expiresIn '${val}' — use '30d', '1y', or seconds as a number`)
24
+ const n = parseInt(m[1], 10)
25
+ const ms = { d: 86400000, h: 3600000, m: 60000, s: 1000, y: 31536000000 }
26
+ return n * ms[m[2]]
27
+ }
28
+
29
+ function credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt }) {
30
+ return enc.encode([
31
+ 'kxco-agent-credential-v1',
32
+ agentKid,
33
+ agentPublicKey,
34
+ sponsorKid,
35
+ agentType,
36
+ label,
37
+ model ?? '',
38
+ canonicalize(scope),
39
+ issuedAt,
40
+ expiresAt,
41
+ ].join('\n'))
42
+ }
43
+
44
+ const VALID_AGENT_TYPES = new Set(['llm', 'robot', 'iot', 'process'])
45
+
46
+ export class KxcoAgentIdentity {
47
+ #kid
48
+ #keypair
49
+ #sponsorKid
50
+ #agentType
51
+ #label
52
+ #model
53
+ #scope
54
+ #issuedAt
55
+ #expiresAt
56
+ #credential
57
+
58
+ constructor(opts) {
59
+ this.#kid = opts.kid
60
+ this.#keypair = opts.keypair
61
+ this.#sponsorKid = opts.sponsorKid
62
+ this.#agentType = opts.agentType
63
+ this.#label = opts.label
64
+ this.#model = opts.model ?? null
65
+ this.#scope = opts.scope
66
+ this.#issuedAt = opts.issuedAt
67
+ this.#expiresAt = opts.expiresAt
68
+ this.#credential = opts.credential
69
+ }
70
+
71
+ get kid() { return this.#kid }
72
+ get sponsorKid() { return this.#sponsorKid }
73
+ get agentType() { return this.#agentType }
74
+ get label() { return this.#label }
75
+ get model() { return this.#model }
76
+ get scope() { return JSON.parse(JSON.stringify(this.#scope)) }
77
+ get issuedAt() { return this.#issuedAt }
78
+ get expiresAt() { return this.#expiresAt }
79
+ get credential() { return JSON.parse(JSON.stringify(this.#credential)) }
80
+
81
+ // ── Factory ───────────────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Create a new agent identity. Any KxcoIdentity holder may sponsor an agent.
85
+ *
86
+ * @param {object} opts
87
+ * @param {{ kid: string, sign(msg: Uint8Array): Promise<Uint8Array> }} opts.sponsor
88
+ * @param {string} opts.label — human-readable name for this agent
89
+ * @param {'llm'|'robot'|'iot'|'process'} opts.agentType
90
+ * @param {string} [opts.model] — model/hardware identifier (optional)
91
+ * @param {object} opts.scope — locked capability manifest (see scope.js)
92
+ * @param {string|number} opts.expiresIn — '30d', '1y', or seconds as number (mandatory)
93
+ * @param {object} [opts.chain] — KxcoChain instance for on-chain registration
94
+ */
95
+ static async create({ sponsor, label, agentType, model, scope, expiresIn, chain } = {}) {
96
+ if (!sponsor?.kid || typeof sponsor.sign !== 'function') {
97
+ throw new KxcoPqAgentError('create: sponsor must have .kid and .sign(message)')
98
+ }
99
+ if (!label) throw new KxcoPqAgentError('create: label is required')
100
+ if (!agentType) throw new KxcoPqAgentError('create: agentType is required')
101
+ if (!VALID_AGENT_TYPES.has(agentType)) {
102
+ throw new KxcoPqAgentError(`create: agentType must be one of: ${[...VALID_AGENT_TYPES].join(', ')}`)
103
+ }
104
+ if (!scope) throw new KxcoPqAgentError('create: scope is required')
105
+ if (expiresIn == null) throw new KxcoPqAgentError('create: expiresIn is required — agents must have an expiry')
106
+
107
+ validateScope(scope)
108
+
109
+ const keypair = mlDsa.ml_dsa65.keygen()
110
+ const agentKid = fingerprint(keypair.publicKey)
111
+ const agentPubB64 = b64url(keypair.publicKey)
112
+ const issuedAt = new Date().toISOString()
113
+ const expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString()
114
+
115
+ const sigMsg = credentialSigningMsg({
116
+ agentKid,
117
+ agentPublicKey: agentPubB64,
118
+ sponsorKid: sponsor.kid,
119
+ agentType,
120
+ label,
121
+ model,
122
+ scope,
123
+ issuedAt,
124
+ expiresAt,
125
+ })
126
+
127
+ const sigBytes = await sponsor.sign(sigMsg)
128
+ const credential = {
129
+ 'kxco-agent': CREDENTIAL_VERSION,
130
+ agentKid,
131
+ agentPublicKey: agentPubB64,
132
+ sponsorKid: sponsor.kid,
133
+ agentType,
134
+ label,
135
+ ...(model && { model }),
136
+ scope,
137
+ issuedAt,
138
+ expiresAt,
139
+ sponsorSignature: b64url(sigBytes),
140
+ }
141
+
142
+ if (chain) {
143
+ const scopeHash = await hashScope(scope)
144
+ const expiresAtSec = Math.floor(new Date(expiresAt).getTime() / 1000)
145
+ await chain.issueAgentCredential({
146
+ agentKid,
147
+ agentPublicKeyHex: Buffer.from(keypair.publicKey).toString('hex'),
148
+ agentType,
149
+ scopeHash,
150
+ expiresAt: expiresAtSec,
151
+ })
152
+ }
153
+
154
+ return new KxcoAgentIdentity({
155
+ kid: agentKid,
156
+ keypair,
157
+ sponsorKid: sponsor.kid,
158
+ agentType,
159
+ label,
160
+ model: model ?? null,
161
+ scope,
162
+ issuedAt,
163
+ expiresAt,
164
+ credential,
165
+ })
166
+ }
167
+
168
+ // ── Signing ───────────────────────────────────────────────────────────────
169
+
170
+ async sign(message) {
171
+ if (!this.#keypair?.secretKey) {
172
+ throw new KxcoPqAgentError('no signing key — reconstruct with KxcoAgentIdentity.import()')
173
+ }
174
+ return Buffer.from(mlDsa.sign(
175
+ new Uint8Array(this.#keypair.secretKey),
176
+ new Uint8Array(message),
177
+ ), 'hex')
178
+ }
179
+
180
+ async getPublicKey() {
181
+ return this.#keypair.publicKey
182
+ }
183
+
184
+ // ── Export / import ───────────────────────────────────────────────────────
185
+
186
+ export() {
187
+ return {
188
+ 'kxco-agent-identity': IDENTITY_VERSION,
189
+ kid: this.#kid,
190
+ sponsorKid: this.#sponsorKid,
191
+ agentType: this.#agentType,
192
+ label: this.#label,
193
+ ...(this.#model && { model: this.#model }),
194
+ scope: this.#scope,
195
+ issuedAt: this.#issuedAt,
196
+ expiresAt: this.#expiresAt,
197
+ secretKey: b64url(this.#keypair.secretKey),
198
+ publicKey: b64url(this.#keypair.publicKey),
199
+ credential: this.#credential,
200
+ }
201
+ }
202
+
203
+ static async import(exported) {
204
+ if (!exported || exported['kxco-agent-identity'] !== IDENTITY_VERSION) {
205
+ throw new KxcoPqAgentError('import: invalid or unsupported agent identity format')
206
+ }
207
+ return new KxcoAgentIdentity({
208
+ kid: exported.kid,
209
+ keypair: { secretKey: fromB64url(exported.secretKey), publicKey: fromB64url(exported.publicKey) },
210
+ sponsorKid: exported.sponsorKid,
211
+ agentType: exported.agentType,
212
+ label: exported.label,
213
+ model: exported.model ?? null,
214
+ scope: exported.scope,
215
+ issuedAt: exported.issuedAt,
216
+ expiresAt: exported.expiresAt,
217
+ credential: exported.credential,
218
+ })
219
+ }
220
+
221
+ // ── Chain client ──────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Returns an AgentChainClient that sends agent-signed intents to the relay.
225
+ * Each request automatically includes this agent's credential and kid.
226
+ * @param {string} relay — relay base URL, e.g. 'https://relay.kxco.ai'
227
+ * @param {{ timeout?: number }} [opts]
228
+ */
229
+ toChainClient(relay, { timeout } = {}) {
230
+ return new AgentChainClient({ relay, agent: this, ...(timeout && { timeout }) })
231
+ }
232
+
233
+ // ── Static: verify a credential ──────────────────────────────────────────
234
+
235
+ /**
236
+ * Verify an agent credential envelope.
237
+ * Pass sponsorPublicKey (Uint8Array) to perform full ML-DSA-65 signature verification.
238
+ * Without it, only expiry and format are checked.
239
+ *
240
+ * @param {object} credential
241
+ * @param {{ sponsorPublicKey?: Uint8Array }} [opts]
242
+ */
243
+ static async verify(credential, { sponsorPublicKey } = {}) {
244
+ if (!credential || credential['kxco-agent'] !== CREDENTIAL_VERSION) {
245
+ return { valid: false, error: 'invalid or unsupported credential format' }
246
+ }
247
+
248
+ const {
249
+ agentKid, agentPublicKey, sponsorKid, agentType,
250
+ label, model, scope, issuedAt, expiresAt, sponsorSignature,
251
+ } = credential
252
+
253
+ if (!agentKid || !agentPublicKey || !sponsorKid || !agentType || !issuedAt || !expiresAt || !sponsorSignature) {
254
+ return { valid: false, error: 'malformed credential — missing required fields' }
255
+ }
256
+
257
+ if (new Date(expiresAt) < new Date()) {
258
+ return { valid: false, error: 'agent credential has expired' }
259
+ }
260
+
261
+ if (sponsorPublicKey) {
262
+ const msg = credentialSigningMsg({ agentKid, agentPublicKey, sponsorKid, agentType, label, model, scope, issuedAt, expiresAt })
263
+ let ok
264
+ try {
265
+ ok = mlDsa.verify(new Uint8Array(sponsorPublicKey), msg, Buffer.from(fromB64url(sponsorSignature)).toString('hex'))
266
+ } catch {
267
+ ok = false
268
+ }
269
+ if (!ok) return { valid: false, error: 'sponsor signature invalid' }
270
+ }
271
+
272
+ return {
273
+ valid: true,
274
+ agentKid,
275
+ sponsorKid,
276
+ agentType,
277
+ label,
278
+ ...(model && { model }),
279
+ scope,
280
+ issuedAt,
281
+ expiresAt,
282
+ }
283
+ }
284
+
285
+ // ── Static: revoke on-chain ───────────────────────────────────────────────
286
+
287
+ /**
288
+ * Revoke an agent credential on-chain.
289
+ * The chain parameter must be a KxcoChain instance belonging to the sponsor.
290
+ *
291
+ * @param {string} agentKid
292
+ * @param {{ chain: object, reason?: string }} opts
293
+ */
294
+ static async revoke(agentKid, { chain, reason = '' } = {}) {
295
+ if (!agentKid) throw new KxcoPqAgentError('revoke: agentKid is required')
296
+ if (!chain) throw new KxcoPqAgentError('revoke: chain is required for on-chain revocation')
297
+ return chain.revokeAgentCredential({ agentKid, reason })
298
+ }
299
+ }