@kadi.build/core 0.13.0 → 0.15.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 +190 -0
- package/agent.json +3 -3
- package/dist/client.d.ts +41 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +78 -0
- package/dist/client.js.map +1 -1
- package/dist/crypto-service.d.ts +100 -0
- package/dist/crypto-service.d.ts.map +1 -0
- package/dist/crypto-service.js +144 -0
- package/dist/crypto-service.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +83 -0
- package/src/crypto-service.ts +169 -0
- package/src/index.ts +3 -0
package/src/client.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { loadStdioTransport } from './transports/stdio.js';
|
|
|
61
61
|
import { loadBrokerTransport } from './transports/broker.js';
|
|
62
62
|
import { AgentJsonManager } from './agent-json.js';
|
|
63
63
|
import { ProcessManager } from './process-manager.js';
|
|
64
|
+
import { CryptoService } from './crypto-service.js';
|
|
64
65
|
|
|
65
66
|
// ═══════════════════════════════════════════════════════════════
|
|
66
67
|
// CONSTANTS
|
|
@@ -206,12 +207,18 @@ export class KadiClient {
|
|
|
206
207
|
/** Registered disconnect hooks */
|
|
207
208
|
private readonly disconnectHooks: Array<() => Promise<void>> = [];
|
|
208
209
|
|
|
210
|
+
/** Registered reconnect hooks */
|
|
211
|
+
private readonly reconnectHooks: Array<(brokerName: string) => void | Promise<void>> = [];
|
|
212
|
+
|
|
209
213
|
/** Lazy-initialized AgentJsonManager */
|
|
210
214
|
private _agentJson: AgentJsonManager | null = null;
|
|
211
215
|
|
|
212
216
|
/** Lazy-initialized ProcessManager */
|
|
213
217
|
private _processes: ProcessManager | null = null;
|
|
214
218
|
|
|
219
|
+
/** Lazy-initialized CryptoService */
|
|
220
|
+
private _crypto: CryptoService | null = null;
|
|
221
|
+
|
|
215
222
|
// ─────────────────────────────────────────────────────────────
|
|
216
223
|
// IDENTITY (single keypair shared across all broker connections)
|
|
217
224
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -1698,9 +1705,36 @@ export class KadiClient {
|
|
|
1698
1705
|
// Finalize connection (heartbeat + status)
|
|
1699
1706
|
this.finalizeConnection(state);
|
|
1700
1707
|
|
|
1708
|
+
// Re-subscribe event patterns that were active before disconnect.
|
|
1709
|
+
// cleanupBroker() cleared subscribedPatterns but preserved eventHandlers,
|
|
1710
|
+
// so we replay broker-side subscriptions for every pattern that still has handlers.
|
|
1711
|
+
if (state.eventHandlers.size > 0) {
|
|
1712
|
+
const patterns = [...state.eventHandlers.keys()];
|
|
1713
|
+
console.error(`[KADI] Re-subscribing ${patterns.length} event pattern(s) on broker "${state.name}"`);
|
|
1714
|
+
for (const pattern of patterns) {
|
|
1715
|
+
try {
|
|
1716
|
+
await this.sendRequest(state, protocol.eventSubscribe(this.nextRequestId++, pattern));
|
|
1717
|
+
state.subscribedPatterns.add(pattern);
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1720
|
+
console.error(`[KADI] Failed to re-subscribe "${pattern}" on broker "${state.name}": ${msg}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1701
1725
|
// Success!
|
|
1702
1726
|
console.error(`[KADI] Reconnected to broker "${state.name}" after ${state.reconnectAttempts} attempts`);
|
|
1703
1727
|
state.reconnectAttempts = 0;
|
|
1728
|
+
|
|
1729
|
+
// Fire reconnect hooks (best-effort, don't let a failing hook break reconnect)
|
|
1730
|
+
for (const hook of this.reconnectHooks) {
|
|
1731
|
+
try {
|
|
1732
|
+
await hook(state.name);
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1735
|
+
console.error(`[KADI] Reconnect hook error for broker "${state.name}": ${msg}`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1704
1738
|
} catch (error) {
|
|
1705
1739
|
// Log the error and try again
|
|
1706
1740
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1722,6 +1756,26 @@ export class KadiClient {
|
|
|
1722
1756
|
this.disconnectHooks.push(hook);
|
|
1723
1757
|
}
|
|
1724
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Register a hook to run after a successful broker reconnection.
|
|
1761
|
+
*
|
|
1762
|
+
* Event subscriptions are automatically re-established on reconnect,
|
|
1763
|
+
* so most agents won't need this. Use it for additional recovery logic:
|
|
1764
|
+
* re-fetching state, logging, sending a "I'm back" message, etc.
|
|
1765
|
+
*
|
|
1766
|
+
* @param hook - Called with the broker name after each successful reconnect.
|
|
1767
|
+
*
|
|
1768
|
+
* @example
|
|
1769
|
+
* ```typescript
|
|
1770
|
+
* client.onReconnect(async (brokerName) => {
|
|
1771
|
+
* console.log(`Reconnected to ${brokerName} — re-initializing state`);
|
|
1772
|
+
* });
|
|
1773
|
+
* ```
|
|
1774
|
+
*/
|
|
1775
|
+
onReconnect(hook: (brokerName: string) => void | Promise<void>): void {
|
|
1776
|
+
this.reconnectHooks.push(hook);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1725
1779
|
/**
|
|
1726
1780
|
* Disconnect from broker(s) and run registered cleanup hooks.
|
|
1727
1781
|
*
|
|
@@ -2481,6 +2535,35 @@ export class KadiClient {
|
|
|
2481
2535
|
return this._processes;
|
|
2482
2536
|
}
|
|
2483
2537
|
|
|
2538
|
+
// ─────────────────────────────────────────────────────────────
|
|
2539
|
+
// CRYPTO SERVICE
|
|
2540
|
+
// ─────────────────────────────────────────────────────────────
|
|
2541
|
+
|
|
2542
|
+
/**
|
|
2543
|
+
* Access the CryptoService for encrypting/decrypting messages.
|
|
2544
|
+
*
|
|
2545
|
+
* Lazily created on first access. Uses NaCl sealed boxes (X25519 +
|
|
2546
|
+
* XSalsa20-Poly1305) derived from this agent's Ed25519 identity keys.
|
|
2547
|
+
*
|
|
2548
|
+
* @example
|
|
2549
|
+
* ```typescript
|
|
2550
|
+
* // Encrypt a message for another agent
|
|
2551
|
+
* const encrypted = client.crypto.encryptFor('secret', otherAgent.publicKey);
|
|
2552
|
+
*
|
|
2553
|
+
* // Decrypt a message sent to this agent
|
|
2554
|
+
* const plaintext = client.crypto.decrypt(encrypted);
|
|
2555
|
+
*
|
|
2556
|
+
* // Get this agent's encryption public key (to share with others)
|
|
2557
|
+
* const pubKey = client.crypto.publicKey;
|
|
2558
|
+
* ```
|
|
2559
|
+
*/
|
|
2560
|
+
get crypto(): CryptoService {
|
|
2561
|
+
if (!this._crypto) {
|
|
2562
|
+
this._crypto = new CryptoService(this._privateKey, this._publicKeyBase64);
|
|
2563
|
+
}
|
|
2564
|
+
return this._crypto;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2484
2567
|
// ─────────────────────────────────────────────────────────────
|
|
2485
2568
|
// UTILITY
|
|
2486
2569
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CryptoService — High-level encryption/decryption for KADI agents.
|
|
3
|
+
*
|
|
4
|
+
* Provides NaCl sealed-box encryption using the agent's Ed25519 identity
|
|
5
|
+
* keys (converted to X25519). Sealed boxes allow anyone with the recipient's
|
|
6
|
+
* public key to encrypt a message that only the recipient can decrypt.
|
|
7
|
+
*
|
|
8
|
+
* This is a stateless wrapper around tweetnacl-sealedbox-js, using the
|
|
9
|
+
* key conversion utilities already in crypto.ts.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Agent A encrypts a message for Agent B
|
|
14
|
+
* const encrypted = agentA.crypto.encryptFor('secret data', agentB.publicKey);
|
|
15
|
+
*
|
|
16
|
+
* // Agent B decrypts
|
|
17
|
+
* const plaintext = agentB.crypto.decrypt(encrypted);
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module crypto-service
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// @ts-expect-error - tweetnacl-sealedbox-js has no type declarations
|
|
24
|
+
import sealedbox from 'tweetnacl-sealedbox-js';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
convertToEncryptionKey,
|
|
28
|
+
convertToEncryptionKeyPair,
|
|
29
|
+
type EncryptionKeyPair,
|
|
30
|
+
} from './crypto.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* High-level encryption/decryption service for inter-agent communication.
|
|
34
|
+
*
|
|
35
|
+
* Uses NaCl sealed boxes (X25519 + XSalsa20-Poly1305 authenticated encryption).
|
|
36
|
+
* Only the intended recipient can decrypt messages.
|
|
37
|
+
*/
|
|
38
|
+
export class CryptoService {
|
|
39
|
+
private readonly _privateKey: Buffer;
|
|
40
|
+
private readonly _publicKeyBase64: string;
|
|
41
|
+
|
|
42
|
+
/** Cached key pairs to avoid repeated conversions */
|
|
43
|
+
private _encryptionKeyPair: EncryptionKeyPair | null = null;
|
|
44
|
+
private _publicKeyX25519Base64: string | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param privateKey - Ed25519 private key in PKCS8 DER format
|
|
48
|
+
* @param publicKeyBase64 - Base64-encoded Ed25519 public key (SPKI DER format)
|
|
49
|
+
*/
|
|
50
|
+
constructor(privateKey: Buffer, publicKeyBase64: string) {
|
|
51
|
+
this._privateKey = privateKey;
|
|
52
|
+
this._publicKeyBase64 = publicKeyBase64;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This agent's X25519 encryption public key (base64).
|
|
57
|
+
*
|
|
58
|
+
* Share this with anyone who needs to encrypt messages for this agent.
|
|
59
|
+
* The key is derived from the agent's Ed25519 identity key.
|
|
60
|
+
*/
|
|
61
|
+
get publicKey(): string {
|
|
62
|
+
if (!this._publicKeyX25519Base64) {
|
|
63
|
+
const raw = convertToEncryptionKey(this._publicKeyBase64);
|
|
64
|
+
this._publicKeyX25519Base64 = Buffer.from(raw).toString('base64');
|
|
65
|
+
}
|
|
66
|
+
return this._publicKeyX25519Base64;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This agent's Ed25519 identity public key (SPKI DER, base64).
|
|
71
|
+
*
|
|
72
|
+
* This is the same key used for broker authentication and agent ID derivation.
|
|
73
|
+
*/
|
|
74
|
+
get identityPublicKey(): string {
|
|
75
|
+
return this._publicKeyBase64;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Encrypt a message so only the specified recipient can decrypt it.
|
|
80
|
+
*
|
|
81
|
+
* Uses NaCl sealed box: the sender generates an ephemeral keypair internally.
|
|
82
|
+
* The recipient cannot determine who sent the message (anonymous encryption).
|
|
83
|
+
* For authentication, use a separate mechanism (e.g., API tokens, signatures).
|
|
84
|
+
*
|
|
85
|
+
* @param plaintext - The string to encrypt
|
|
86
|
+
* @param recipientPublicKey - Recipient's public key. Accepts either:
|
|
87
|
+
* - Ed25519 SPKI DER base64 (the standard `client.publicKey` format) — auto-converted to X25519
|
|
88
|
+
* - Raw X25519 base64 (32 bytes when decoded) — used directly
|
|
89
|
+
* @returns Base64-encoded ciphertext (NaCl sealed box)
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // Encrypt for another agent using their Ed25519 identity key
|
|
94
|
+
* const encrypted = client.crypto.encryptFor(
|
|
95
|
+
* JSON.stringify({ API_KEY: 'sk-123', DB_PASS: 'hunter2' }),
|
|
96
|
+
* otherAgent.publicKey
|
|
97
|
+
* );
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
encryptFor(plaintext: string, recipientPublicKey: string): string {
|
|
101
|
+
const x25519Key = this.resolveToX25519(recipientPublicKey);
|
|
102
|
+
const message = new TextEncoder().encode(plaintext);
|
|
103
|
+
const sealed = sealedbox.seal(message, x25519Key);
|
|
104
|
+
return Buffer.from(sealed).toString('base64');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Decrypt a message that was encrypted for this agent.
|
|
109
|
+
*
|
|
110
|
+
* The message must have been encrypted using this agent's public key
|
|
111
|
+
* (either via `encryptFor()` or directly with NaCl sealed box).
|
|
112
|
+
*
|
|
113
|
+
* @param ciphertext - Base64-encoded NaCl sealed box ciphertext
|
|
114
|
+
* @returns The decrypted plaintext string
|
|
115
|
+
* @throws Error if decryption fails (wrong key, corrupted data, or tampered message)
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const plaintext = client.crypto.decrypt(encryptedBase64);
|
|
120
|
+
* const secrets = JSON.parse(plaintext);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
decrypt(ciphertext: string): string {
|
|
124
|
+
const keyPair = this.getEncryptionKeyPair();
|
|
125
|
+
const sealed = new Uint8Array(Buffer.from(ciphertext, 'base64'));
|
|
126
|
+
|
|
127
|
+
const decrypted = sealedbox.open(sealed, keyPair.publicKey, keyPair.secretKey);
|
|
128
|
+
|
|
129
|
+
if (!decrypted) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
'Decryption failed: invalid ciphertext, wrong key, or tampered message'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new TextDecoder().decode(decrypted);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────────────────────
|
|
139
|
+
// INTERNAL
|
|
140
|
+
// ─────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a public key to X25519 format.
|
|
144
|
+
* Handles both Ed25519 SPKI DER base64 and raw X25519 base64.
|
|
145
|
+
*/
|
|
146
|
+
private resolveToX25519(publicKey: string): Uint8Array {
|
|
147
|
+
const raw = Buffer.from(publicKey, 'base64');
|
|
148
|
+
|
|
149
|
+
// Raw X25519 key is exactly 32 bytes
|
|
150
|
+
// SPKI DER Ed25519 key is 44 bytes (12-byte header + 32-byte key)
|
|
151
|
+
if (raw.length === 32) {
|
|
152
|
+
return new Uint8Array(raw);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Assume SPKI DER format — convert Ed25519 → X25519
|
|
156
|
+
return convertToEncryptionKey(publicKey);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get or create cached encryption key pair */
|
|
160
|
+
private getEncryptionKeyPair(): EncryptionKeyPair {
|
|
161
|
+
if (!this._encryptionKeyPair) {
|
|
162
|
+
this._encryptionKeyPair = convertToEncryptionKeyPair(
|
|
163
|
+
this._privateKey,
|
|
164
|
+
this._publicKeyBase64
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return this._encryptionKeyPair;
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -119,6 +119,9 @@ export { AgentJsonManager } from './agent-json.js';
|
|
|
119
119
|
// Process manager
|
|
120
120
|
export { ProcessManager, ManagedProcess } from './process-manager.js';
|
|
121
121
|
|
|
122
|
+
// Crypto service (sealed-box encryption for inter-agent communication)
|
|
123
|
+
export { CryptoService } from './crypto-service.js';
|
|
124
|
+
|
|
122
125
|
// Dot-path utilities
|
|
123
126
|
export { getByPath, setByPath, deleteByPath, deepMerge } from './utils.js';
|
|
124
127
|
|