@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,345 @@
1
+ /**
2
+ * Visa TAP (Trusted Agent Protocol) for AURA Scout SDK
3
+ *
4
+ * Implements Visa's Trusted Agent Protocol for agent identity
5
+ * verification in commerce transactions.
6
+ *
7
+ * Key Features:
8
+ * - Agent registration with Visa directory
9
+ * - HTTP message signing for transaction requests
10
+ * - Identity verification for merchants/payment networks
11
+ *
12
+ * @see https://usa.visa.com/about-visa/newsroom/press-releases.releaseId.21716.html
13
+ */
14
+
15
+ import { createHash, createSign, createVerify, generateKeyPairSync, randomBytes } from 'crypto';
16
+
17
+ // TAP Registry URL (would be Visa's in production)
18
+ const TAP_REGISTRY_URL = process.env.TAP_REGISTRY_URL || 'https://tap.visa.com/v1';
19
+
20
+ /**
21
+ * Visa TAP - Trusted Agent Protocol implementation
22
+ */
23
+ export class VisaTAP {
24
+ /**
25
+ * Generate a new agent key pair
26
+ *
27
+ * @returns {Object} Public and private keys
28
+ */
29
+ static generateKeyPair() {
30
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
31
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
32
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
33
+ });
34
+
35
+ // Also generate key ID
36
+ const keyId = createHash('sha256')
37
+ .update(publicKey)
38
+ .digest('hex')
39
+ .substring(0, 16);
40
+
41
+ return {
42
+ keyId,
43
+ publicKey,
44
+ privateKey,
45
+ algorithm: 'ed25519',
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Register an agent with the TAP directory
51
+ *
52
+ * @param {Object} params
53
+ * @param {string} params.agentId - Unique agent identifier
54
+ * @param {string} params.publicKey - Agent's public key (PEM format)
55
+ * @param {Object} params.metadata - Agent metadata
56
+ * @returns {Object} Registration response with TAP ID
57
+ */
58
+ static async register({
59
+ agentId,
60
+ publicKey,
61
+ metadata = {},
62
+ }) {
63
+ const registration = {
64
+ agentId,
65
+ publicKey,
66
+ metadata: {
67
+ name: metadata.name || 'AURA Agent',
68
+ operator: metadata.operator || 'AURA Labs',
69
+ capabilities: metadata.capabilities || ['shopping', 'payments'],
70
+ version: metadata.version || '1.0',
71
+ registeredAt: new Date().toISOString(),
72
+ },
73
+ };
74
+
75
+ // In production, this would POST to Visa's TAP registry
76
+ // For development, simulate registration
77
+ if (process.env.NODE_ENV === 'production') {
78
+ const response = await fetch(`${TAP_REGISTRY_URL}/agents/register`, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify(registration),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ throw new TAPError('Registration failed', await response.text());
86
+ }
87
+
88
+ return response.json();
89
+ }
90
+
91
+ // Development mode - simulate registration
92
+ const tapId = `tap_${agentId}_${randomBytes(8).toString('hex')}`;
93
+ return {
94
+ tapId,
95
+ agentId,
96
+ status: 'active',
97
+ registeredAt: new Date().toISOString(),
98
+ expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), // 1 year
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Sign an HTTP request for TAP verification
104
+ *
105
+ * Creates a signature that proves the request came from
106
+ * a registered agent acting on behalf of a user.
107
+ *
108
+ * @param {Object} request - Request to sign
109
+ * @param {string} request.method - HTTP method
110
+ * @param {string} request.url - Request URL
111
+ * @param {Object} request.headers - Request headers
112
+ * @param {Object} request.body - Request body
113
+ * @param {Object} credentials - TAP credentials
114
+ * @param {string} credentials.tapId - TAP registration ID
115
+ * @param {string} credentials.privateKey - Agent's private key
116
+ * @param {string} credentials.keyId - Key identifier
117
+ */
118
+ static async signRequest(request, credentials) {
119
+ const { method, url, headers = {}, body } = request;
120
+ const { tapId, privateKey, keyId } = credentials;
121
+
122
+ // Create signature input
123
+ const timestamp = Math.floor(Date.now() / 1000);
124
+ const nonce = randomBytes(16).toString('hex');
125
+
126
+ // Build signature base string (HTTP Message Signatures spec)
127
+ const signatureInput = this.#buildSignatureInput({
128
+ method,
129
+ url,
130
+ headers,
131
+ body,
132
+ timestamp,
133
+ nonce,
134
+ tapId,
135
+ });
136
+
137
+ // Sign the input
138
+ const signature = this.#sign(signatureInput, privateKey);
139
+
140
+ // Build signature header
141
+ const signatureHeader = this.#buildSignatureHeader({
142
+ keyId,
143
+ algorithm: 'ed25519',
144
+ timestamp,
145
+ nonce,
146
+ signature,
147
+ });
148
+
149
+ // Return signed request with TAP headers
150
+ return {
151
+ ...request,
152
+ headers: {
153
+ ...headers,
154
+ 'X-TAP-Agent-Id': tapId,
155
+ 'X-TAP-Timestamp': timestamp.toString(),
156
+ 'X-TAP-Nonce': nonce,
157
+ 'Signature': signatureHeader,
158
+ 'Signature-Input': `sig=("@method" "@path" "@authority" "x-tap-agent-id" "x-tap-timestamp" "x-tap-nonce");keyid="${keyId}";alg="ed25519";created=${timestamp}`,
159
+ },
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Verify a TAP-signed request
165
+ *
166
+ * Used by merchants/payment networks to verify agent identity.
167
+ *
168
+ * @param {Object} request - Request to verify
169
+ * @param {Function} publicKeyLookup - Function to get public key from TAP registry
170
+ */
171
+ static async verifyRequest(request, publicKeyLookup) {
172
+ const tapId = request.headers['x-tap-agent-id'];
173
+ const timestamp = parseInt(request.headers['x-tap-timestamp']);
174
+ const nonce = request.headers['x-tap-nonce'];
175
+ const signatureHeader = request.headers['signature'];
176
+
177
+ if (!tapId || !timestamp || !nonce || !signatureHeader) {
178
+ return { valid: false, error: 'Missing TAP headers' };
179
+ }
180
+
181
+ // Check timestamp freshness (within 5 minutes)
182
+ const now = Math.floor(Date.now() / 1000);
183
+ if (Math.abs(now - timestamp) > 300) {
184
+ return { valid: false, error: 'Request timestamp too old or in future' };
185
+ }
186
+
187
+ // Look up public key from TAP registry
188
+ let publicKey;
189
+ try {
190
+ publicKey = await publicKeyLookup(tapId);
191
+ } catch (error) {
192
+ return { valid: false, error: 'Agent not found in TAP registry' };
193
+ }
194
+
195
+ // Parse signature header
196
+ const signature = this.#parseSignatureHeader(signatureHeader);
197
+
198
+ // Rebuild signature input
199
+ const signatureInput = this.#buildSignatureInput({
200
+ method: request.method,
201
+ url: request.url,
202
+ headers: request.headers,
203
+ body: request.body,
204
+ timestamp,
205
+ nonce,
206
+ tapId,
207
+ });
208
+
209
+ // Verify signature
210
+ try {
211
+ const isValid = this.#verify(signatureInput, signature, publicKey);
212
+ return {
213
+ valid: isValid,
214
+ agentId: tapId,
215
+ timestamp: new Date(timestamp * 1000).toISOString(),
216
+ };
217
+ } catch (error) {
218
+ return { valid: false, error: error.message };
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Create TAP credentials object for use in transactions
224
+ */
225
+ static createCredentials({ tapId, keyPair }) {
226
+ return {
227
+ tapId,
228
+ privateKey: keyPair.privateKey,
229
+ publicKey: keyPair.publicKey,
230
+ keyId: keyPair.keyId,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Rotate agent keys (create new key pair and update registry)
236
+ */
237
+ static async rotateKeys(tapId, oldPrivateKey) {
238
+ const newKeyPair = this.generateKeyPair();
239
+
240
+ if (process.env.NODE_ENV === 'production') {
241
+ // Sign the rotation request with old key
242
+ const rotationRequest = {
243
+ tapId,
244
+ newPublicKey: newKeyPair.publicKey,
245
+ timestamp: new Date().toISOString(),
246
+ };
247
+
248
+ const signature = this.#sign(
249
+ JSON.stringify(rotationRequest),
250
+ oldPrivateKey
251
+ );
252
+
253
+ const response = await fetch(`${TAP_REGISTRY_URL}/agents/${tapId}/keys/rotate`, {
254
+ method: 'POST',
255
+ headers: {
256
+ 'Content-Type': 'application/json',
257
+ 'X-TAP-Signature': signature,
258
+ },
259
+ body: JSON.stringify(rotationRequest),
260
+ });
261
+
262
+ if (!response.ok) {
263
+ throw new TAPError('Key rotation failed', await response.text());
264
+ }
265
+ }
266
+
267
+ return newKeyPair;
268
+ }
269
+
270
+ // =========================================================================
271
+ // Private Methods
272
+ // =========================================================================
273
+
274
+ /**
275
+ * Build signature input string per HTTP Message Signatures spec
276
+ */
277
+ static #buildSignatureInput({ method, url, headers, body, timestamp, nonce, tapId }) {
278
+ const urlObj = new URL(url, 'https://example.com');
279
+
280
+ const components = [
281
+ `"@method": ${method.toUpperCase()}`,
282
+ `"@path": ${urlObj.pathname}`,
283
+ `"@authority": ${urlObj.host}`,
284
+ `"x-tap-agent-id": ${tapId}`,
285
+ `"x-tap-timestamp": ${timestamp}`,
286
+ `"x-tap-nonce": ${nonce}`,
287
+ ];
288
+
289
+ // Include content-digest for requests with body
290
+ if (body) {
291
+ const bodyHash = createHash('sha256')
292
+ .update(typeof body === 'string' ? body : JSON.stringify(body))
293
+ .digest('base64');
294
+ components.push(`"content-digest": sha-256=:${bodyHash}:`);
295
+ }
296
+
297
+ return components.join('\n');
298
+ }
299
+
300
+ /**
301
+ * Build signature header value
302
+ */
303
+ static #buildSignatureHeader({ keyId, algorithm, timestamp, nonce, signature }) {
304
+ return `sig=:${signature}:`;
305
+ }
306
+
307
+ /**
308
+ * Parse signature from header
309
+ */
310
+ static #parseSignatureHeader(header) {
311
+ const match = header.match(/sig=:([^:]+):/);
312
+ return match ? match[1] : null;
313
+ }
314
+
315
+ /**
316
+ * Sign data with private key
317
+ */
318
+ static #sign(data, privateKey) {
319
+ const signer = createSign('sha256');
320
+ signer.update(data);
321
+ return signer.sign(privateKey, 'base64');
322
+ }
323
+
324
+ /**
325
+ * Verify signature with public key
326
+ */
327
+ static #verify(data, signature, publicKey) {
328
+ const verifier = createVerify('sha256');
329
+ verifier.update(data);
330
+ return verifier.verify(publicKey, signature, 'base64');
331
+ }
332
+ }
333
+
334
+ /**
335
+ * TAP Protocol Error
336
+ */
337
+ export class TAPError extends Error {
338
+ constructor(message, details) {
339
+ super(message);
340
+ this.name = 'TAPError';
341
+ this.details = details;
342
+ }
343
+ }
344
+
345
+ export default VisaTAP;
@@ -0,0 +1,157 @@
1
+ # Scout SDK Tests
2
+
3
+ This directory contains tests for the AURA Scout SDK protocol implementations.
4
+
5
+ ## Test Structure
6
+
7
+ ```
8
+ tests/
9
+ ├── mcp-client.test.js # Unit tests for MCP Client
10
+ ├── ap2-mandates.test.js # Unit tests for AP2 Mandates
11
+ ├── visa-tap.test.js # Unit tests for Visa TAP
12
+ ├── run-protocol-tests.js # Runner for unit tests
13
+ └── scenarios/ # Real-world scenario tests
14
+ ├── index.js # Scenario test runner
15
+ ├── ap2-scenarios.test.js
16
+ ├── tap-scenarios.test.js
17
+ ├── mcp-scenarios.test.js
18
+ └── integration-scenarios.test.js
19
+ ```
20
+
21
+ ## Running Tests
22
+
23
+ ### All Tests
24
+ ```bash
25
+ npm test
26
+ ```
27
+
28
+ ### Protocol Unit Tests
29
+ ```bash
30
+ npm run test:protocols
31
+ ```
32
+
33
+ ### Scenario Tests
34
+ ```bash
35
+ # All scenarios
36
+ npm run test:scenarios
37
+
38
+ # Individual protocol scenarios
39
+ npm run test:ap2
40
+ npm run test:tap
41
+ npm run test:mcp
42
+ npm run test:integration
43
+ ```
44
+
45
+ ### Direct Node.js Execution
46
+ ```bash
47
+ node --test src/tests/scenarios/ap2-scenarios.test.js
48
+ ```
49
+
50
+ ## Test Categories
51
+
52
+ ### Unit Tests (`*.test.js`)
53
+ Basic functionality tests for each protocol implementation:
54
+ - Function inputs/outputs
55
+ - Data structures
56
+ - Error handling
57
+ - API compliance
58
+
59
+ ### Scenario Tests (`scenarios/*.test.js`)
60
+ Real-world usage scenario tests:
61
+
62
+ #### AP2 Scenarios (`ap2-scenarios.test.js`)
63
+ - Complete shopping flow (Intent → Cart → Payment)
64
+ - Budget limit enforcement
65
+ - Category restrictions
66
+ - Merchant allowlist/blocklist
67
+ - Time-limited authorization
68
+ - Currency handling
69
+ - Multiple constraint violations
70
+ - Signature verification
71
+
72
+ #### TAP Scenarios (`tap-scenarios.test.js`)
73
+ - Agent registration flow
74
+ - Payment request signing
75
+ - Request verification by merchants
76
+ - Multiple agent identities
77
+ - Key rotation
78
+ - Replay attack protection
79
+ - Different HTTP request types
80
+ - Error handling
81
+
82
+ #### MCP Scenarios (`mcp-scenarios.test.js`)
83
+ - Client initialization
84
+ - Tool discovery
85
+ - Server connection management
86
+ - Event handling
87
+ - Context aggregation
88
+ - Protocol compliance
89
+ - Multiple server support
90
+ - AURA integration patterns
91
+
92
+ #### Integration Scenarios (`integration-scenarios.test.js`)
93
+ - Complete agentic shopping flow (MCP + AP2 + TAP)
94
+ - Multi-offer comparison with constraints
95
+ - Payment network verification
96
+ - MCP context enriching intent
97
+ - Error recovery in protocol chain
98
+ - Time-sensitive transactions
99
+ - Agent capabilities advertisement
100
+
101
+ ## Test on Railway (Remote)
102
+
103
+ For environments without local Node.js, tests can be run via the Core API:
104
+
105
+ ```bash
106
+ # Check dev routes enabled
107
+ curl https://aura-labsai-production.up.railway.app/dev/status
108
+
109
+ # Run inline protocol tests
110
+ curl https://aura-labsai-production.up.railway.app/dev/test/protocols
111
+ ```
112
+
113
+ ## Writing New Tests
114
+
115
+ ### Unit Test Template
116
+ ```javascript
117
+ import { test, describe } from 'node:test';
118
+ import assert from 'node:assert';
119
+ import { SomeModule } from '../path/to/module.js';
120
+
121
+ describe('Module Name', () => {
122
+ test('does something specific', () => {
123
+ const result = SomeModule.doSomething();
124
+ assert.strictEqual(result, expected);
125
+ });
126
+ });
127
+ ```
128
+
129
+ ### Scenario Test Template
130
+ ```javascript
131
+ import { test, describe } from 'node:test';
132
+ import assert from 'node:assert';
133
+
134
+ describe('Scenario: Real World Use Case', () => {
135
+ let sharedState;
136
+
137
+ test('Step 1: Setup', async () => {
138
+ sharedState = await setupSomething();
139
+ assert.ok(sharedState);
140
+ });
141
+
142
+ test('Step 2: Action', async () => {
143
+ const result = await doAction(sharedState);
144
+ assert.strictEqual(result.status, 'success');
145
+ });
146
+
147
+ test('Step 3: Verify', () => {
148
+ assert.ok(sharedState.completed);
149
+ });
150
+ });
151
+ ```
152
+
153
+ ## Protocol References
154
+
155
+ - **MCP**: [Model Context Protocol Specification](https://modelcontextprotocol.io/specification)
156
+ - **AP2**: [Google Agent Payments Protocol](https://ap2-protocol.org/specification/)
157
+ - **Visa TAP**: [Visa Trusted Agent Protocol](https://usa.visa.com/about-visa/newsroom/press-releases.releaseId.21716.html)