@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.
- package/README.md +237 -0
- package/bin/scout-cli.js +325 -0
- package/examples/basic-purchase.js +112 -0
- package/examples/protocol-integration.js +294 -0
- package/package.json +63 -0
- package/src/activity.js +420 -0
- package/src/ap2/mandates.js +366 -0
- package/src/client.js +217 -0
- package/src/errors.js +66 -0
- package/src/index.js +655 -0
- package/src/intent-session.js +233 -0
- package/src/key-manager.js +204 -0
- package/src/key-manager.test.js +293 -0
- package/src/mcp/client.js +458 -0
- package/src/session.js +603 -0
- package/src/tap/visa.js +345 -0
- package/src/tests/README.md +157 -0
- package/src/tests/ap2-mandates.test.js +413 -0
- package/src/tests/intent-session.test.js +292 -0
- package/src/tests/mcp-client.test.js +224 -0
- package/src/tests/ping.test.js +282 -0
- package/src/tests/run-protocol-tests.js +57 -0
- package/src/tests/scenarios/ap2-scenarios.test.js +422 -0
- package/src/tests/scenarios/index.js +60 -0
- package/src/tests/scenarios/integration-scenarios.test.js +514 -0
- package/src/tests/scenarios/mcp-scenarios.test.js +419 -0
- package/src/tests/scenarios/tap-scenarios.test.js +461 -0
- package/src/tests/visa-tap.test.js +376 -0
|
@@ -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
|
+
});
|