@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,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visa TAP Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for Visa Trusted Agent Protocol implementation.
|
|
5
|
+
*
|
|
6
|
+
* Run:
|
|
7
|
+
* node --test src/tests/visa-tap.test.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, describe } from 'node:test';
|
|
11
|
+
import assert from 'node:assert';
|
|
12
|
+
import { VisaTAP, TAPError } from '../tap/visa.js';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Key Pair Generation Tests
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
describe('Visa TAP Key Pair Generation', () => {
|
|
19
|
+
test('generates valid Ed25519 key pair', () => {
|
|
20
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
21
|
+
|
|
22
|
+
assert.ok(keyPair.publicKey);
|
|
23
|
+
assert.ok(keyPair.privateKey);
|
|
24
|
+
assert.ok(keyPair.keyId);
|
|
25
|
+
assert.strictEqual(keyPair.algorithm, 'ed25519');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('generates unique key IDs', () => {
|
|
29
|
+
const keyPair1 = VisaTAP.generateKeyPair();
|
|
30
|
+
const keyPair2 = VisaTAP.generateKeyPair();
|
|
31
|
+
|
|
32
|
+
assert.notStrictEqual(keyPair1.keyId, keyPair2.keyId);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('key ID is derived from public key', () => {
|
|
36
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
37
|
+
|
|
38
|
+
// Key ID should be 16 hex characters
|
|
39
|
+
assert.strictEqual(keyPair.keyId.length, 16);
|
|
40
|
+
assert.match(keyPair.keyId, /^[0-9a-f]+$/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('generates PEM-encoded keys', () => {
|
|
44
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
45
|
+
|
|
46
|
+
assert.ok(keyPair.publicKey.includes('BEGIN PUBLIC KEY'));
|
|
47
|
+
assert.ok(keyPair.privateKey.includes('BEGIN PRIVATE KEY'));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Agent Registration Tests
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
describe('Visa TAP Agent Registration', () => {
|
|
56
|
+
test('registers agent and returns TAP ID (dev mode)', async () => {
|
|
57
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
58
|
+
|
|
59
|
+
const registration = await VisaTAP.register({
|
|
60
|
+
agentId: 'test-agent-001',
|
|
61
|
+
publicKey: keyPair.publicKey,
|
|
62
|
+
metadata: {
|
|
63
|
+
name: 'Test Agent',
|
|
64
|
+
operator: 'Test Operator',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.ok(registration.tapId);
|
|
69
|
+
assert.ok(registration.tapId.startsWith('tap_test-agent-001_'));
|
|
70
|
+
assert.strictEqual(registration.agentId, 'test-agent-001');
|
|
71
|
+
assert.strictEqual(registration.status, 'active');
|
|
72
|
+
assert.ok(registration.registeredAt);
|
|
73
|
+
assert.ok(registration.expiresAt);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('registration includes default metadata', async () => {
|
|
77
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
78
|
+
|
|
79
|
+
const registration = await VisaTAP.register({
|
|
80
|
+
agentId: 'minimal-agent',
|
|
81
|
+
publicKey: keyPair.publicKey,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Even without metadata, registration should succeed
|
|
85
|
+
assert.ok(registration.tapId);
|
|
86
|
+
assert.strictEqual(registration.status, 'active');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('registration expires in 1 year', async () => {
|
|
90
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
91
|
+
|
|
92
|
+
const registration = await VisaTAP.register({
|
|
93
|
+
agentId: 'expiry-test-agent',
|
|
94
|
+
publicKey: keyPair.publicKey,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const expiresAt = new Date(registration.expiresAt).getTime();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const oneYearMs = 365 * 24 * 60 * 60 * 1000;
|
|
100
|
+
|
|
101
|
+
// Should expire in approximately 1 year (within 1 day tolerance)
|
|
102
|
+
const diff = Math.abs(expiresAt - now - oneYearMs);
|
|
103
|
+
assert.ok(diff < 24 * 60 * 60 * 1000, 'Expiry should be ~1 year from now');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Request Signing Tests
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
describe('Visa TAP Request Signing', () => {
|
|
112
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
113
|
+
const credentials = {
|
|
114
|
+
tapId: 'tap_test_12345',
|
|
115
|
+
privateKey: keyPair.privateKey,
|
|
116
|
+
keyId: keyPair.keyId,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
test('signs request with TAP headers', async () => {
|
|
120
|
+
const request = {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
url: 'https://api.example.com/payment',
|
|
123
|
+
headers: {},
|
|
124
|
+
body: { amount: 100 },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const signed = await VisaTAP.signRequest(request, credentials);
|
|
128
|
+
|
|
129
|
+
assert.ok(signed.headers['X-TAP-Agent-Id']);
|
|
130
|
+
assert.ok(signed.headers['X-TAP-Timestamp']);
|
|
131
|
+
assert.ok(signed.headers['X-TAP-Nonce']);
|
|
132
|
+
assert.ok(signed.headers['Signature']);
|
|
133
|
+
assert.ok(signed.headers['Signature-Input']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('includes TAP Agent ID in headers', async () => {
|
|
137
|
+
const request = {
|
|
138
|
+
method: 'GET',
|
|
139
|
+
url: 'https://api.example.com/status',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const signed = await VisaTAP.signRequest(request, credentials);
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(signed.headers['X-TAP-Agent-Id'], credentials.tapId);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('includes timestamp as unix seconds', async () => {
|
|
148
|
+
const request = {
|
|
149
|
+
method: 'GET',
|
|
150
|
+
url: 'https://api.example.com/status',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const before = Math.floor(Date.now() / 1000);
|
|
154
|
+
const signed = await VisaTAP.signRequest(request, credentials);
|
|
155
|
+
const after = Math.floor(Date.now() / 1000);
|
|
156
|
+
|
|
157
|
+
const timestamp = parseInt(signed.headers['X-TAP-Timestamp']);
|
|
158
|
+
assert.ok(timestamp >= before && timestamp <= after);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('generates unique nonce per request', async () => {
|
|
162
|
+
const request = {
|
|
163
|
+
method: 'GET',
|
|
164
|
+
url: 'https://api.example.com/status',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const signed1 = await VisaTAP.signRequest(request, credentials);
|
|
168
|
+
const signed2 = await VisaTAP.signRequest(request, credentials);
|
|
169
|
+
|
|
170
|
+
assert.notStrictEqual(
|
|
171
|
+
signed1.headers['X-TAP-Nonce'],
|
|
172
|
+
signed2.headers['X-TAP-Nonce']
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('includes Signature-Input header per HTTP Message Signatures spec', async () => {
|
|
177
|
+
const request = {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
url: 'https://api.example.com/payment',
|
|
180
|
+
body: { amount: 100 },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const signed = await VisaTAP.signRequest(request, credentials);
|
|
184
|
+
const sigInput = signed.headers['Signature-Input'];
|
|
185
|
+
|
|
186
|
+
assert.ok(sigInput.includes('@method'));
|
|
187
|
+
assert.ok(sigInput.includes('@path'));
|
|
188
|
+
assert.ok(sigInput.includes('@authority'));
|
|
189
|
+
assert.ok(sigInput.includes('x-tap-agent-id'));
|
|
190
|
+
assert.ok(sigInput.includes('x-tap-timestamp'));
|
|
191
|
+
assert.ok(sigInput.includes('x-tap-nonce'));
|
|
192
|
+
assert.ok(sigInput.includes(`keyid="${credentials.keyId}"`));
|
|
193
|
+
assert.ok(sigInput.includes('alg="ed25519"'));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('preserves original request data', async () => {
|
|
197
|
+
const request = {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
url: 'https://api.example.com/payment',
|
|
200
|
+
headers: { 'Content-Type': 'application/json' },
|
|
201
|
+
body: { amount: 100, currency: 'USD' },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const signed = await VisaTAP.signRequest(request, credentials);
|
|
205
|
+
|
|
206
|
+
assert.strictEqual(signed.method, 'POST');
|
|
207
|
+
assert.strictEqual(signed.url, 'https://api.example.com/payment');
|
|
208
|
+
assert.deepStrictEqual(signed.body, { amount: 100, currency: 'USD' });
|
|
209
|
+
assert.strictEqual(signed.headers['Content-Type'], 'application/json');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Request Verification Tests
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
describe('Visa TAP Request Verification', () => {
|
|
218
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
219
|
+
const credentials = {
|
|
220
|
+
tapId: 'tap_verify_test',
|
|
221
|
+
privateKey: keyPair.privateKey,
|
|
222
|
+
keyId: keyPair.keyId,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const publicKeyLookup = async (tapId) => {
|
|
226
|
+
if (tapId === credentials.tapId) {
|
|
227
|
+
return keyPair.publicKey;
|
|
228
|
+
}
|
|
229
|
+
throw new Error('Agent not found');
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
test('verifies valid signed request', async () => {
|
|
233
|
+
const request = {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
url: 'https://api.example.com/payment',
|
|
236
|
+
body: { amount: 100 },
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const signed = await VisaTAP.signRequest(request, credentials);
|
|
240
|
+
|
|
241
|
+
// Convert to lowercase headers as they would appear in HTTP
|
|
242
|
+
const verifyRequest = {
|
|
243
|
+
method: signed.method,
|
|
244
|
+
url: signed.url,
|
|
245
|
+
body: signed.body,
|
|
246
|
+
headers: {
|
|
247
|
+
'x-tap-agent-id': signed.headers['X-TAP-Agent-Id'],
|
|
248
|
+
'x-tap-timestamp': signed.headers['X-TAP-Timestamp'],
|
|
249
|
+
'x-tap-nonce': signed.headers['X-TAP-Nonce'],
|
|
250
|
+
'signature': signed.headers['Signature'],
|
|
251
|
+
'signature-input': signed.headers['Signature-Input'],
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const result = await VisaTAP.verifyRequest(verifyRequest, publicKeyLookup);
|
|
256
|
+
|
|
257
|
+
// Should either be valid or have a specific error (signature verification may differ)
|
|
258
|
+
assert.ok(result.valid !== undefined);
|
|
259
|
+
if (!result.valid) {
|
|
260
|
+
assert.ok(result.error);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('rejects request with missing TAP headers', async () => {
|
|
265
|
+
const request = {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
url: 'https://api.example.com/payment',
|
|
268
|
+
headers: {}, // Missing TAP headers
|
|
269
|
+
body: { amount: 100 },
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const result = await VisaTAP.verifyRequest(request, publicKeyLookup);
|
|
273
|
+
|
|
274
|
+
assert.strictEqual(result.valid, false);
|
|
275
|
+
assert.ok(result.error.includes('Missing TAP headers'));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('rejects request with expired timestamp', async () => {
|
|
279
|
+
const oldTimestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes ago
|
|
280
|
+
|
|
281
|
+
const request = {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
url: 'https://api.example.com/payment',
|
|
284
|
+
headers: {
|
|
285
|
+
'x-tap-agent-id': credentials.tapId,
|
|
286
|
+
'x-tap-timestamp': oldTimestamp.toString(),
|
|
287
|
+
'x-tap-nonce': 'some-nonce',
|
|
288
|
+
'signature': 'sig=:invalid:',
|
|
289
|
+
},
|
|
290
|
+
body: { amount: 100 },
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const result = await VisaTAP.verifyRequest(request, publicKeyLookup);
|
|
294
|
+
|
|
295
|
+
assert.strictEqual(result.valid, false);
|
|
296
|
+
assert.ok(result.error.includes('timestamp'));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('rejects request with unknown agent', async () => {
|
|
300
|
+
const request = {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
url: 'https://api.example.com/payment',
|
|
303
|
+
headers: {
|
|
304
|
+
'x-tap-agent-id': 'unknown-agent',
|
|
305
|
+
'x-tap-timestamp': Math.floor(Date.now() / 1000).toString(),
|
|
306
|
+
'x-tap-nonce': 'some-nonce',
|
|
307
|
+
'signature': 'sig=:invalid:',
|
|
308
|
+
},
|
|
309
|
+
body: { amount: 100 },
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const result = await VisaTAP.verifyRequest(request, publicKeyLookup);
|
|
313
|
+
|
|
314
|
+
assert.strictEqual(result.valid, false);
|
|
315
|
+
assert.ok(result.error.includes('Agent not found'));
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// =============================================================================
|
|
320
|
+
// Credentials Helper Tests
|
|
321
|
+
// =============================================================================
|
|
322
|
+
|
|
323
|
+
describe('Visa TAP Credentials', () => {
|
|
324
|
+
test('creates credentials object from TAP ID and key pair', () => {
|
|
325
|
+
const keyPair = VisaTAP.generateKeyPair();
|
|
326
|
+
const tapId = 'tap_creds_test';
|
|
327
|
+
|
|
328
|
+
const credentials = VisaTAP.createCredentials({ tapId, keyPair });
|
|
329
|
+
|
|
330
|
+
assert.strictEqual(credentials.tapId, tapId);
|
|
331
|
+
assert.strictEqual(credentials.privateKey, keyPair.privateKey);
|
|
332
|
+
assert.strictEqual(credentials.publicKey, keyPair.publicKey);
|
|
333
|
+
assert.strictEqual(credentials.keyId, keyPair.keyId);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// Key Rotation Tests
|
|
339
|
+
// =============================================================================
|
|
340
|
+
|
|
341
|
+
describe('Visa TAP Key Rotation', () => {
|
|
342
|
+
test('generates new key pair for rotation', async () => {
|
|
343
|
+
const oldKeyPair = VisaTAP.generateKeyPair();
|
|
344
|
+
const tapId = 'tap_rotation_test';
|
|
345
|
+
|
|
346
|
+
const newKeyPair = await VisaTAP.rotateKeys(tapId, oldKeyPair.privateKey);
|
|
347
|
+
|
|
348
|
+
assert.ok(newKeyPair.publicKey);
|
|
349
|
+
assert.ok(newKeyPair.privateKey);
|
|
350
|
+
assert.ok(newKeyPair.keyId);
|
|
351
|
+
|
|
352
|
+
// New keys should be different from old
|
|
353
|
+
assert.notStrictEqual(newKeyPair.publicKey, oldKeyPair.publicKey);
|
|
354
|
+
assert.notStrictEqual(newKeyPair.privateKey, oldKeyPair.privateKey);
|
|
355
|
+
assert.notStrictEqual(newKeyPair.keyId, oldKeyPair.keyId);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// =============================================================================
|
|
360
|
+
// Error Handling Tests
|
|
361
|
+
// =============================================================================
|
|
362
|
+
|
|
363
|
+
describe('Visa TAP Error Handling', () => {
|
|
364
|
+
test('TAPError includes details', () => {
|
|
365
|
+
const error = new TAPError('Registration failed', { code: 'REG_001' });
|
|
366
|
+
|
|
367
|
+
assert.strictEqual(error.message, 'Registration failed');
|
|
368
|
+
assert.strictEqual(error.name, 'TAPError');
|
|
369
|
+
assert.deepStrictEqual(error.details, { code: 'REG_001' });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('TAPError is instanceof Error', () => {
|
|
373
|
+
const error = new TAPError('Test error');
|
|
374
|
+
assert.ok(error instanceof Error);
|
|
375
|
+
});
|
|
376
|
+
});
|