@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,422 @@
1
+ /**
2
+ * AP2 Protocol Scenario Tests
3
+ *
4
+ * Real-world scenario tests for Google's Agent Payments Protocol (AP2).
5
+ * These tests verify the full mandate lifecycle in common shopping scenarios.
6
+ *
7
+ * Run:
8
+ * node --test src/tests/scenarios/ap2-scenarios.test.js
9
+ */
10
+
11
+ import { test, describe } from 'node:test';
12
+ import assert from 'node:assert';
13
+ import { AP2Mandates, generateKeyPair } from '../../ap2/mandates.js';
14
+
15
+ // =============================================================================
16
+ // Test Setup
17
+ // =============================================================================
18
+
19
+ const userKeys = generateKeyPair();
20
+ const agentKeys = generateKeyPair();
21
+
22
+ // =============================================================================
23
+ // Scenario 1: Basic Shopping Flow
24
+ // =============================================================================
25
+
26
+ describe('Scenario: Basic Shopping Flow', () => {
27
+ let intentMandate;
28
+ let cartMandate;
29
+ let paymentMandate;
30
+
31
+ test('Step 1: User creates intent mandate for shopping', async () => {
32
+ intentMandate = await AP2Mandates.createIntent({
33
+ agentId: 'shopping-scout-001',
34
+ userId: 'user-alice-001',
35
+ userKey: userKeys.privateKey,
36
+ constraints: {
37
+ maxAmount: 500,
38
+ currency: 'USD',
39
+ categories: ['electronics', 'office-supplies'],
40
+ validUntil: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
41
+ },
42
+ metadata: {
43
+ purpose: 'Office supplies shopping',
44
+ },
45
+ });
46
+
47
+ assert.strictEqual(intentMandate.type, 'intent');
48
+ assert.strictEqual(intentMandate.constraints.maxAmount, 500);
49
+ assert.ok(intentMandate.proof.proofValue, 'Should be signed');
50
+ });
51
+
52
+ test('Step 2: Agent finds offer within constraints', async () => {
53
+ const offer = {
54
+ id: 'offer-laptop-stand-001',
55
+ beaconId: 'beacon-techmart',
56
+ beaconName: 'TechMart Office',
57
+ product: { name: 'Ergonomic Laptop Stand', sku: 'ERGO-LS-100' },
58
+ unitPrice: 79.99,
59
+ quantity: 2,
60
+ totalPrice: 159.98,
61
+ currency: 'USD',
62
+ deliveryDate: '2026-02-15',
63
+ };
64
+
65
+ // Validate offer against intent
66
+ const validation = AP2Mandates.validateIntentCoverage(intentMandate, {
67
+ totalAmount: offer.totalPrice,
68
+ currency: offer.currency,
69
+ category: 'office-supplies',
70
+ merchantId: offer.beaconId,
71
+ });
72
+
73
+ assert.strictEqual(validation.valid, true);
74
+ assert.strictEqual(validation.errors.length, 0);
75
+ });
76
+
77
+ test('Step 3: User approves specific purchase with cart mandate', async () => {
78
+ const offer = {
79
+ id: 'offer-laptop-stand-001',
80
+ beaconId: 'beacon-techmart',
81
+ beaconName: 'TechMart Office',
82
+ product: { name: 'Ergonomic Laptop Stand', sku: 'ERGO-LS-100' },
83
+ unitPrice: 79.99,
84
+ quantity: 2,
85
+ totalPrice: 159.98,
86
+ currency: 'USD',
87
+ deliveryDate: '2026-02-15',
88
+ };
89
+
90
+ cartMandate = await AP2Mandates.createCart({
91
+ sessionId: 'session-shop-001',
92
+ offer,
93
+ userKey: userKeys.privateKey,
94
+ userId: 'user-alice-001',
95
+ intentMandateId: intentMandate.id,
96
+ });
97
+
98
+ assert.strictEqual(cartMandate.type, 'cart');
99
+ assert.strictEqual(cartMandate.intentMandateRef, intentMandate.id);
100
+ assert.strictEqual(cartMandate.cart.totalAmount, 159.98);
101
+ assert.strictEqual(cartMandate.userPresent, true);
102
+ });
103
+
104
+ test('Step 4: Agent creates payment mandate for processor', async () => {
105
+ paymentMandate = await AP2Mandates.createPayment({
106
+ cartMandate,
107
+ paymentMethod: { type: 'card', network: 'visa' },
108
+ agentId: 'shopping-scout-001',
109
+ agentKey: agentKeys.privateKey,
110
+ tapCredentials: { tapId: 'tap_scout_001_abc123' },
111
+ });
112
+
113
+ assert.strictEqual(paymentMandate.type, 'payment');
114
+ assert.strictEqual(paymentMandate.cartMandateRef, cartMandate.id);
115
+ assert.strictEqual(paymentMandate.agent.tapId, 'tap_scout_001_abc123');
116
+ assert.strictEqual(paymentMandate.transaction.amount, 159.98);
117
+ assert.ok(paymentMandate.riskSignals.intentMandatePresent);
118
+ });
119
+
120
+ test('Step 5: Verify complete audit trail', () => {
121
+ // Intent -> Cart -> Payment chain is intact
122
+ assert.strictEqual(cartMandate.intentMandateRef, intentMandate.id);
123
+ assert.strictEqual(paymentMandate.cartMandateRef, cartMandate.id);
124
+
125
+ // All mandates are signed
126
+ assert.ok(intentMandate.proof.proofValue);
127
+ assert.ok(cartMandate.proof.proofValue);
128
+ assert.ok(paymentMandate.proof.proofValue);
129
+ });
130
+ });
131
+
132
+ // =============================================================================
133
+ // Scenario 2: Budget Limit Enforcement
134
+ // =============================================================================
135
+
136
+ describe('Scenario: Budget Limit Enforcement', () => {
137
+ test('Rejects purchase exceeding budget', async () => {
138
+ const intentMandate = await AP2Mandates.createIntent({
139
+ agentId: 'budget-scout',
140
+ userId: 'user-bob',
141
+ userKey: userKeys.privateKey,
142
+ constraints: {
143
+ maxAmount: 100,
144
+ currency: 'USD',
145
+ },
146
+ });
147
+
148
+ const expensiveOffer = {
149
+ totalAmount: 250,
150
+ currency: 'USD',
151
+ };
152
+
153
+ const validation = AP2Mandates.validateIntentCoverage(intentMandate, expensiveOffer);
154
+
155
+ assert.strictEqual(validation.valid, false);
156
+ assert.ok(validation.errors[0].includes('exceeds max'));
157
+ });
158
+
159
+ test('Allows purchase at exact budget limit', async () => {
160
+ const intentMandate = await AP2Mandates.createIntent({
161
+ agentId: 'budget-scout',
162
+ userId: 'user-bob',
163
+ userKey: userKeys.privateKey,
164
+ constraints: {
165
+ maxAmount: 100,
166
+ currency: 'USD',
167
+ },
168
+ });
169
+
170
+ const exactOffer = {
171
+ totalAmount: 100,
172
+ currency: 'USD',
173
+ };
174
+
175
+ const validation = AP2Mandates.validateIntentCoverage(intentMandate, exactOffer);
176
+
177
+ assert.strictEqual(validation.valid, true);
178
+ });
179
+ });
180
+
181
+ // =============================================================================
182
+ // Scenario 3: Category Restrictions
183
+ // =============================================================================
184
+
185
+ describe('Scenario: Category Restrictions', () => {
186
+ let intentMandate;
187
+
188
+ test('Setup: Create intent with category restrictions', async () => {
189
+ intentMandate = await AP2Mandates.createIntent({
190
+ agentId: 'category-scout',
191
+ userId: 'user-carol',
192
+ userKey: userKeys.privateKey,
193
+ constraints: {
194
+ maxAmount: 1000,
195
+ currency: 'USD',
196
+ categories: ['office-supplies', 'electronics'],
197
+ },
198
+ });
199
+
200
+ assert.deepStrictEqual(intentMandate.constraints.categories, ['office-supplies', 'electronics']);
201
+ });
202
+
203
+ test('Allows purchase in permitted category', () => {
204
+ const validation = AP2Mandates.validateIntentCoverage(intentMandate, {
205
+ totalAmount: 200,
206
+ currency: 'USD',
207
+ category: 'electronics',
208
+ });
209
+
210
+ assert.strictEqual(validation.valid, true);
211
+ });
212
+
213
+ test('Rejects purchase in forbidden category', () => {
214
+ const validation = AP2Mandates.validateIntentCoverage(intentMandate, {
215
+ totalAmount: 200,
216
+ currency: 'USD',
217
+ category: 'luxury-goods',
218
+ });
219
+
220
+ assert.strictEqual(validation.valid, false);
221
+ assert.ok(validation.errors[0].includes('Category'));
222
+ });
223
+ });
224
+
225
+ // =============================================================================
226
+ // Scenario 4: Merchant Allowlist/Blocklist
227
+ // =============================================================================
228
+
229
+ describe('Scenario: Merchant Restrictions', () => {
230
+ test('Allowlist: Only permits specified merchants', async () => {
231
+ const intentMandate = await AP2Mandates.createIntent({
232
+ agentId: 'merchant-scout',
233
+ userId: 'user-dave',
234
+ userKey: userKeys.privateKey,
235
+ constraints: {
236
+ maxAmount: 500,
237
+ merchantAllowlist: ['trusted-vendor-1', 'trusted-vendor-2'],
238
+ },
239
+ });
240
+
241
+ // Trusted merchant allowed
242
+ const trustedValidation = AP2Mandates.validateIntentCoverage(intentMandate, {
243
+ totalAmount: 100,
244
+ merchantId: 'trusted-vendor-1',
245
+ });
246
+ assert.strictEqual(trustedValidation.valid, true);
247
+
248
+ // Unknown merchant blocked
249
+ const unknownValidation = AP2Mandates.validateIntentCoverage(intentMandate, {
250
+ totalAmount: 100,
251
+ merchantId: 'random-vendor',
252
+ });
253
+ assert.strictEqual(unknownValidation.valid, false);
254
+ });
255
+
256
+ test('Blocklist: Blocks specified merchants', async () => {
257
+ const intentMandate = await AP2Mandates.createIntent({
258
+ agentId: 'blocklist-scout',
259
+ userId: 'user-eve',
260
+ userKey: userKeys.privateKey,
261
+ constraints: {
262
+ maxAmount: 500,
263
+ merchantBlocklist: ['sketchy-vendor', 'banned-seller'],
264
+ },
265
+ });
266
+
267
+ // Normal merchant allowed
268
+ const normalValidation = AP2Mandates.validateIntentCoverage(intentMandate, {
269
+ totalAmount: 100,
270
+ merchantId: 'normal-store',
271
+ });
272
+ assert.strictEqual(normalValidation.valid, true);
273
+
274
+ // Blocked merchant rejected
275
+ const blockedValidation = AP2Mandates.validateIntentCoverage(intentMandate, {
276
+ totalAmount: 100,
277
+ merchantId: 'sketchy-vendor',
278
+ });
279
+ assert.strictEqual(blockedValidation.valid, false);
280
+ assert.ok(blockedValidation.errors[0].includes('blocklisted'));
281
+ });
282
+ });
283
+
284
+ // =============================================================================
285
+ // Scenario 5: Time-Limited Authorization
286
+ // =============================================================================
287
+
288
+ describe('Scenario: Time-Limited Authorization', () => {
289
+ test('Rejects expired intent mandate', async () => {
290
+ const expiredMandate = await AP2Mandates.createIntent({
291
+ agentId: 'time-scout',
292
+ userId: 'user-frank',
293
+ userKey: userKeys.privateKey,
294
+ constraints: {
295
+ maxAmount: 500,
296
+ validUntil: new Date(Date.now() - 1000).toISOString(), // Already expired
297
+ },
298
+ });
299
+
300
+ const validation = AP2Mandates.validateIntentCoverage(expiredMandate, {
301
+ totalAmount: 100,
302
+ });
303
+
304
+ assert.strictEqual(validation.valid, false);
305
+ assert.ok(validation.errors[0].includes('expired'));
306
+ });
307
+
308
+ test('Rejects mandate not yet valid', async () => {
309
+ const futureMandate = await AP2Mandates.createIntent({
310
+ agentId: 'time-scout',
311
+ userId: 'user-grace',
312
+ userKey: userKeys.privateKey,
313
+ constraints: {
314
+ maxAmount: 500,
315
+ validFrom: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now
316
+ validUntil: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
317
+ },
318
+ });
319
+
320
+ const validation = AP2Mandates.validateIntentCoverage(futureMandate, {
321
+ totalAmount: 100,
322
+ });
323
+
324
+ assert.strictEqual(validation.valid, false);
325
+ assert.ok(validation.errors[0].includes('not yet valid'));
326
+ });
327
+ });
328
+
329
+ // =============================================================================
330
+ // Scenario 6: Currency Mismatch
331
+ // =============================================================================
332
+
333
+ describe('Scenario: Currency Handling', () => {
334
+ test('Rejects mismatched currency', async () => {
335
+ const usdMandate = await AP2Mandates.createIntent({
336
+ agentId: 'currency-scout',
337
+ userId: 'user-henry',
338
+ userKey: userKeys.privateKey,
339
+ constraints: {
340
+ maxAmount: 500,
341
+ currency: 'USD',
342
+ },
343
+ });
344
+
345
+ const euroOffer = {
346
+ totalAmount: 100,
347
+ currency: 'EUR',
348
+ };
349
+
350
+ const validation = AP2Mandates.validateIntentCoverage(usdMandate, euroOffer);
351
+
352
+ assert.strictEqual(validation.valid, false);
353
+ assert.ok(validation.errors[0].includes('Currency'));
354
+ });
355
+ });
356
+
357
+ // =============================================================================
358
+ // Scenario 7: Multiple Constraint Violations
359
+ // =============================================================================
360
+
361
+ describe('Scenario: Multiple Constraint Violations', () => {
362
+ test('Reports all violations', async () => {
363
+ const strictMandate = await AP2Mandates.createIntent({
364
+ agentId: 'strict-scout',
365
+ userId: 'user-iris',
366
+ userKey: userKeys.privateKey,
367
+ constraints: {
368
+ maxAmount: 100,
369
+ currency: 'USD',
370
+ categories: ['electronics'],
371
+ merchantBlocklist: ['bad-vendor'],
372
+ validUntil: new Date(Date.now() - 1000).toISOString(), // Expired
373
+ },
374
+ });
375
+
376
+ const badOffer = {
377
+ totalAmount: 500, // Over budget
378
+ currency: 'EUR', // Wrong currency
379
+ category: 'furniture', // Wrong category
380
+ merchantId: 'bad-vendor', // Blocklisted
381
+ };
382
+
383
+ const validation = AP2Mandates.validateIntentCoverage(strictMandate, badOffer);
384
+
385
+ assert.strictEqual(validation.valid, false);
386
+ assert.ok(validation.errors.length >= 4, 'Should report multiple errors');
387
+ });
388
+ });
389
+
390
+ // =============================================================================
391
+ // Scenario 8: Signature Verification
392
+ // =============================================================================
393
+
394
+ describe('Scenario: Mandate Signature Verification', () => {
395
+ test('Verifies valid mandate signature', async () => {
396
+ const mandate = await AP2Mandates.createIntent({
397
+ agentId: 'verify-scout',
398
+ userId: 'user-jack',
399
+ userKey: userKeys.privateKey,
400
+ constraints: { maxAmount: 100 },
401
+ });
402
+
403
+ const result = await AP2Mandates.verify(mandate, userKeys.publicKey);
404
+
405
+ // In dev mode uses mock signatures
406
+ assert.ok(result.valid !== undefined);
407
+ });
408
+
409
+ test('Detects mandate without signature', async () => {
410
+ const unsignedMandate = {
411
+ type: 'intent',
412
+ id: 'fake-mandate',
413
+ constraints: { maxAmount: 100 },
414
+ // No proof field
415
+ };
416
+
417
+ const result = await AP2Mandates.verify(unsignedMandate, userKeys.publicKey);
418
+
419
+ assert.strictEqual(result.valid, false);
420
+ assert.ok(result.error.includes('No proof'));
421
+ });
422
+ });
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Protocol Scenario Test Runner
5
+ *
6
+ * Runs all protocol scenario tests for the Scout SDK:
7
+ * - AP2 Mandates scenarios
8
+ * - Visa TAP scenarios
9
+ * - MCP Client scenarios
10
+ * - Integration scenarios
11
+ *
12
+ * Usage:
13
+ * node src/tests/scenarios/index.js
14
+ *
15
+ * Or via npm:
16
+ * npm run test:scenarios
17
+ */
18
+
19
+ import { spawn } from 'child_process';
20
+ import { fileURLToPath } from 'url';
21
+ import { dirname, join } from 'path';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+
26
+ const scenarioFiles = [
27
+ 'ap2-scenarios.test.js',
28
+ 'tap-scenarios.test.js',
29
+ 'mcp-scenarios.test.js',
30
+ 'integration-scenarios.test.js',
31
+ ];
32
+
33
+ console.log('\n' + '═'.repeat(70));
34
+ console.log(' AURA Scout SDK - Protocol Scenario Tests');
35
+ console.log('═'.repeat(70) + '\n');
36
+
37
+ console.log('Running scenario tests for:');
38
+ console.log(' • AP2 Mandates (Google Agent Payments Protocol)');
39
+ console.log(' • Visa TAP (Trusted Agent Protocol)');
40
+ console.log(' • MCP Client (Model Context Protocol)');
41
+ console.log(' • Integration (All protocols combined)');
42
+ console.log('');
43
+
44
+ const testPaths = scenarioFiles.map(f => join(__dirname, f));
45
+
46
+ const child = spawn('node', ['--test', ...testPaths], {
47
+ stdio: 'inherit',
48
+ cwd: join(__dirname, '../../..'),
49
+ });
50
+
51
+ child.on('exit', (code) => {
52
+ console.log('\n' + '═'.repeat(70));
53
+ if (code === 0) {
54
+ console.log(' ✅ All scenario tests passed!');
55
+ } else {
56
+ console.log(' ❌ Some tests failed');
57
+ }
58
+ console.log('═'.repeat(70) + '\n');
59
+ process.exit(code);
60
+ });