@aura-labs-ai/scout 0.2.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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Tests for Agent Key Manager — Ed25519 Identity Management
3
+ *
4
+ * Covers: key generation, persistence via storage adapter, signing,
5
+ * request signing, fingerprints, and idempotent initialization.
6
+ */
7
+
8
+ import { describe, it, beforeEach } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import crypto from 'node:crypto';
11
+ import { KeyManager, MemoryStorage } from './key-manager.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Test helpers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Convert raw 32-byte base64 public key to Node.js KeyObject for verification */
18
+ function rawPublicKeyToKeyObject(base64Key) {
19
+ const prefix = Buffer.from('302a300506032b6570032100', 'hex');
20
+ const rawBytes = Buffer.from(base64Key, 'base64');
21
+ const derKey = Buffer.concat([prefix, rawBytes]);
22
+ return crypto.createPublicKey({ key: derKey, format: 'der', type: 'spki' });
23
+ }
24
+
25
+ /** Verify an Ed25519 signature using a raw base64 public key */
26
+ function verifySignature(publicKeyBase64, data, signatureBase64) {
27
+ const keyObject = rawPublicKeyToKeyObject(publicKeyBase64);
28
+ const message = Buffer.from(data);
29
+ const signature = Buffer.from(signatureBase64, 'base64');
30
+ return crypto.verify(null, message, keyObject, signature);
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // MemoryStorage
35
+ // ---------------------------------------------------------------------------
36
+
37
+ describe('MemoryStorage', () => {
38
+ it('should get/set/remove values', async () => {
39
+ const storage = new MemoryStorage();
40
+
41
+ assert.equal(await storage.get('missing'), null);
42
+
43
+ await storage.set('key1', 'value1');
44
+ assert.equal(await storage.get('key1'), 'value1');
45
+
46
+ await storage.remove('key1');
47
+ assert.equal(await storage.get('key1'), null);
48
+ });
49
+ });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // KeyManager — Initialization
53
+ // ---------------------------------------------------------------------------
54
+
55
+ describe('KeyManager init', () => {
56
+ let km;
57
+
58
+ beforeEach(() => {
59
+ km = new KeyManager();
60
+ });
61
+
62
+ it('should generate a new key pair on first init', async () => {
63
+ const { publicKey, isNew } = await km.init();
64
+
65
+ assert.equal(isNew, true);
66
+ assert.equal(typeof publicKey, 'string');
67
+
68
+ // Public key should be 32 bytes base64
69
+ const rawBytes = Buffer.from(publicKey, 'base64');
70
+ assert.equal(rawBytes.length, 32);
71
+ });
72
+
73
+ it('should return isNew=false on subsequent init calls', async () => {
74
+ const first = await km.init();
75
+ const second = await km.init();
76
+
77
+ assert.equal(first.isNew, true);
78
+ assert.equal(second.isNew, false);
79
+ assert.equal(first.publicKey, second.publicKey);
80
+ });
81
+
82
+ it('should load existing keys from storage on new KeyManager instance', async () => {
83
+ const storage = new MemoryStorage();
84
+ const km1 = new KeyManager({ storage });
85
+ const { publicKey: pk1 } = await km1.init();
86
+
87
+ // New KeyManager with same storage should load the same key
88
+ const km2 = new KeyManager({ storage });
89
+ const { publicKey: pk2, isNew } = await km2.init();
90
+
91
+ assert.equal(pk2, pk1);
92
+ assert.equal(isNew, false);
93
+ });
94
+
95
+ it('should use custom storage prefix', async () => {
96
+ const storage = new MemoryStorage();
97
+ const km1 = new KeyManager({ storage, storagePrefix: 'app1:' });
98
+ const km2 = new KeyManager({ storage, storagePrefix: 'app2:' });
99
+
100
+ await km1.init();
101
+ await km2.init();
102
+
103
+ // Different prefixes should produce different keys
104
+ assert.notEqual(km1.publicKey, km2.publicKey);
105
+ });
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // KeyManager — Signing
110
+ // ---------------------------------------------------------------------------
111
+
112
+ describe('KeyManager sign', () => {
113
+ let km;
114
+
115
+ beforeEach(async () => {
116
+ km = new KeyManager();
117
+ await km.init();
118
+ });
119
+
120
+ it('should produce a valid Ed25519 signature', () => {
121
+ const data = 'hello world';
122
+ const sig = km.sign(data);
123
+
124
+ // Signature should be 64 bytes base64
125
+ const sigBytes = Buffer.from(sig, 'base64');
126
+ assert.equal(sigBytes.length, 64);
127
+
128
+ // Should verify with the public key
129
+ const valid = verifySignature(km.publicKey, data, sig);
130
+ assert.equal(valid, true);
131
+ });
132
+
133
+ it('should produce different signatures for different data', () => {
134
+ const sig1 = km.sign('data1');
135
+ const sig2 = km.sign('data2');
136
+ assert.notEqual(sig1, sig2);
137
+ });
138
+
139
+ it('should produce consistent signatures (deterministic Ed25519)', () => {
140
+ const sig1 = km.sign('same data');
141
+ const sig2 = km.sign('same data');
142
+ // Ed25519 is deterministic — same key + same data = same signature
143
+ assert.equal(sig1, sig2);
144
+ });
145
+
146
+ it('should throw if not initialized', () => {
147
+ const uninit = new KeyManager();
148
+ assert.throws(() => uninit.sign('data'), /not initialized/);
149
+ });
150
+ });
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // KeyManager — Request Signing
154
+ // ---------------------------------------------------------------------------
155
+
156
+ describe('KeyManager signRequest', () => {
157
+ let km;
158
+
159
+ beforeEach(async () => {
160
+ km = new KeyManager();
161
+ await km.init();
162
+ });
163
+
164
+ it('should return signature and timestamp for GET request', () => {
165
+ const result = km.signRequest({ method: 'GET', path: '/health', body: null });
166
+
167
+ assert.equal(typeof result.signature, 'string');
168
+ assert.equal(typeof result.timestamp, 'string');
169
+ assert.equal(Buffer.from(result.signature, 'base64').length, 64);
170
+
171
+ // Timestamp should be a recent unix ms
172
+ const ts = parseInt(result.timestamp, 10);
173
+ assert.ok(Math.abs(Date.now() - ts) < 1000);
174
+ });
175
+
176
+ it('should produce verifiable signature for POST request', () => {
177
+ const body = JSON.stringify({ intent: 'buy widgets' });
178
+ const { signature, timestamp } = km.signRequest({
179
+ method: 'POST',
180
+ path: '/sessions',
181
+ body,
182
+ });
183
+
184
+ // Reconstruct the signing string server-side would build
185
+ const bodyDigest = crypto.createHash('sha256').update(body).digest('base64');
186
+ const signingString = `POST\n/sessions\n${timestamp}\n${bodyDigest}`;
187
+
188
+ const valid = verifySignature(km.publicKey, signingString, signature);
189
+ assert.equal(valid, true);
190
+ });
191
+
192
+ it('should produce verifiable signature for GET (no body)', () => {
193
+ const { signature, timestamp } = km.signRequest({
194
+ method: 'GET',
195
+ path: '/sessions/123',
196
+ body: null,
197
+ });
198
+
199
+ const signingString = `GET\n/sessions/123\n${timestamp}\n`;
200
+ const valid = verifySignature(km.publicKey, signingString, signature);
201
+ assert.equal(valid, true);
202
+ });
203
+ });
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // KeyManager — Fingerprint
207
+ // ---------------------------------------------------------------------------
208
+
209
+ describe('KeyManager fingerprint', () => {
210
+ it('should return SHA-256 hex of the public key', async () => {
211
+ const km = new KeyManager();
212
+ await km.init();
213
+
214
+ const fp = km.fingerprint;
215
+ assert.equal(fp.length, 64);
216
+ assert.match(fp, /^[0-9a-f]{64}$/);
217
+
218
+ // Should match manual computation
219
+ const expected = crypto.createHash('sha256')
220
+ .update(Buffer.from(km.publicKey, 'base64'))
221
+ .digest('hex');
222
+ assert.equal(fp, expected);
223
+ });
224
+ });
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // KeyManager — Agent ID storage
228
+ // ---------------------------------------------------------------------------
229
+
230
+ describe('KeyManager agentId storage', () => {
231
+ it('should store and retrieve agentId', async () => {
232
+ const km = new KeyManager();
233
+ await km.init();
234
+
235
+ assert.equal(await km.getAgentId(), null);
236
+
237
+ await km.setAgentId('test-uuid-123');
238
+ assert.equal(await km.getAgentId(), 'test-uuid-123');
239
+ });
240
+
241
+ it('should persist agentId across KeyManager instances with shared storage', async () => {
242
+ const storage = new MemoryStorage();
243
+ const km1 = new KeyManager({ storage });
244
+ await km1.init();
245
+ await km1.setAgentId('my-agent-id');
246
+
247
+ const km2 = new KeyManager({ storage });
248
+ await km2.init();
249
+ assert.equal(await km2.getAgentId(), 'my-agent-id');
250
+ });
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // KeyManager — Cross-compatibility with server verification
255
+ // ---------------------------------------------------------------------------
256
+
257
+ describe('KeyManager cross-compat with agent-auth.js signing scheme', () => {
258
+ it('should produce registration signatures the server can verify', async () => {
259
+ const km = new KeyManager();
260
+ await km.init();
261
+
262
+ const body = JSON.stringify({
263
+ publicKey: km.publicKey,
264
+ type: 'scout',
265
+ manifest: { name: 'test' },
266
+ });
267
+
268
+ const signature = km.sign(body);
269
+
270
+ // Server-side verification: verifyRegistrationSignature(publicKey, sig, body)
271
+ const valid = verifySignature(km.publicKey, body, signature);
272
+ assert.equal(valid, true);
273
+ });
274
+
275
+ it('should produce request signatures the server can verify', async () => {
276
+ const km = new KeyManager();
277
+ await km.init();
278
+
279
+ const body = JSON.stringify({ intent: 'test intent' });
280
+ const { signature, timestamp } = km.signRequest({
281
+ method: 'POST',
282
+ path: '/sessions',
283
+ body,
284
+ });
285
+
286
+ // Server reconstructs: buildSigningString(method, path, timestamp, body)
287
+ const bodyDigest = crypto.createHash('sha256').update(body).digest('base64');
288
+ const signingString = `POST\n/sessions\n${timestamp}\n${bodyDigest}`;
289
+
290
+ const valid = verifySignature(km.publicKey, signingString, signature);
291
+ assert.equal(valid, true);
292
+ });
293
+ });