@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
package/src/activity.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scout Activity Logger
|
|
3
|
+
*
|
|
4
|
+
* Structured observability for a single Scout's (buyer) lifecycle.
|
|
5
|
+
* Every action the Scout takes — registration, session management,
|
|
6
|
+
* offer evaluation, transaction handling — is recorded as a structured event
|
|
7
|
+
* with timestamps, durations, correlation IDs, and outcome metadata.
|
|
8
|
+
*
|
|
9
|
+
* Consumers can:
|
|
10
|
+
* - Subscribe to events in real-time via `on(eventType, handler)`
|
|
11
|
+
* - Query the activity log via `getEvents()` with filters
|
|
12
|
+
* - Get summary stats via `getSummary()`
|
|
13
|
+
* - Plug in a custom logger (pino, winston, console, etc.)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```js
|
|
17
|
+
* const scout = createScout({ ... });
|
|
18
|
+
*
|
|
19
|
+
* // Listen to specific events
|
|
20
|
+
* scout.activity.on('offer.committed', (event) => {
|
|
21
|
+
* console.log(`Offer ${event.offerId} committed in ${event.durationMs}ms`);
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Get summary stats
|
|
25
|
+
* const stats = scout.activity.getSummary();
|
|
26
|
+
* console.log(`Sessions: ${stats.sessions.created}, Offers: ${stats.offers.committed}`);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { randomUUID } from 'crypto';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Supported Scout activity event types
|
|
34
|
+
*/
|
|
35
|
+
export const ScoutActivityEventTypes = {
|
|
36
|
+
// Lifecycle
|
|
37
|
+
SCOUT_CREATED: 'scout.created',
|
|
38
|
+
SCOUT_READY: 'scout.ready',
|
|
39
|
+
SCOUT_READY_FAILED: 'scout.ready_failed',
|
|
40
|
+
SCOUT_REGISTERED: 'scout.registered',
|
|
41
|
+
SCOUT_REGISTRATION_FAILED: 'scout.registration_failed',
|
|
42
|
+
|
|
43
|
+
// Intent
|
|
44
|
+
INTENT_CREATED: 'intent.created',
|
|
45
|
+
INTENT_FAILED: 'intent.failed',
|
|
46
|
+
|
|
47
|
+
// Sessions
|
|
48
|
+
SESSION_CREATED: 'session.created',
|
|
49
|
+
SESSION_REFRESHED: 'session.refreshed',
|
|
50
|
+
SESSION_RESUMED: 'session.resumed',
|
|
51
|
+
SESSION_RESUME_FAILED: 'session.resume_failed',
|
|
52
|
+
SESSION_CANCELLED: 'session.cancelled',
|
|
53
|
+
SESSION_CANCEL_FAILED: 'session.cancel_failed',
|
|
54
|
+
SESSION_COMMITTED: 'session.committed',
|
|
55
|
+
SESSION_COMMIT_FAILED: 'session.commit_failed',
|
|
56
|
+
|
|
57
|
+
// Offers
|
|
58
|
+
OFFERS_POLLING: 'offers.polling',
|
|
59
|
+
OFFERS_RECEIVED: 'offers.received',
|
|
60
|
+
OFFERS_EVALUATED: 'offers.evaluated',
|
|
61
|
+
OFFER_COMMITTED: 'offer.committed',
|
|
62
|
+
WAIT_FOR_OFFERS: 'offers.wait_complete',
|
|
63
|
+
WAIT_FOR_OFFERS_FAILED: 'offers.wait_failed',
|
|
64
|
+
|
|
65
|
+
// Transactions
|
|
66
|
+
TRANSACTION_CREATED: 'transaction.created',
|
|
67
|
+
TRANSACTION_REFRESHED: 'transaction.refreshed',
|
|
68
|
+
TRANSACTION_FULFILLED: 'transaction.fulfilled',
|
|
69
|
+
TRANSACTION_FULFILLMENT_TIMEOUT: 'transaction.fulfillment_timeout',
|
|
70
|
+
WAIT_FOR_FULFILLMENT: 'transaction.wait_complete',
|
|
71
|
+
WAIT_FOR_FULFILLMENT_FAILED: 'transaction.wait_failed',
|
|
72
|
+
|
|
73
|
+
// Healthcheck
|
|
74
|
+
PING_SUCCESS: 'ping.success',
|
|
75
|
+
PING_FAILED: 'ping.failed',
|
|
76
|
+
|
|
77
|
+
// HTTP
|
|
78
|
+
REQUEST_START: 'request.start',
|
|
79
|
+
REQUEST_COMPLETE: 'request.complete',
|
|
80
|
+
REQUEST_FAILED: 'request.failed',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Activity event — the unit of observability
|
|
85
|
+
*/
|
|
86
|
+
export class ActivityEvent {
|
|
87
|
+
constructor(type, data = {}) {
|
|
88
|
+
this.id = randomUUID();
|
|
89
|
+
this.type = type;
|
|
90
|
+
this.timestamp = new Date().toISOString();
|
|
91
|
+
this.epochMs = Date.now();
|
|
92
|
+
this.durationMs = data.durationMs || null;
|
|
93
|
+
this.correlationId = data.correlationId || null;
|
|
94
|
+
this.sessionId = data.sessionId || null;
|
|
95
|
+
this.transactionId = data.transactionId || null;
|
|
96
|
+
this.success = data.success !== undefined ? data.success : null;
|
|
97
|
+
this.error = data.error || null;
|
|
98
|
+
this.metadata = data.metadata || {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toJSON() {
|
|
102
|
+
const obj = {
|
|
103
|
+
id: this.id,
|
|
104
|
+
type: this.type,
|
|
105
|
+
timestamp: this.timestamp,
|
|
106
|
+
};
|
|
107
|
+
if (this.durationMs !== null) obj.durationMs = this.durationMs;
|
|
108
|
+
if (this.correlationId) obj.correlationId = this.correlationId;
|
|
109
|
+
if (this.sessionId) obj.sessionId = this.sessionId;
|
|
110
|
+
if (this.transactionId) obj.transactionId = this.transactionId;
|
|
111
|
+
if (this.success !== null) obj.success = this.success;
|
|
112
|
+
if (this.error) obj.error = this.error;
|
|
113
|
+
if (Object.keys(this.metadata).length > 0) obj.metadata = this.metadata;
|
|
114
|
+
return obj;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* ScoutActivityLogger — records and emits structured Scout events
|
|
120
|
+
*/
|
|
121
|
+
export class ScoutActivityLogger {
|
|
122
|
+
#events = [];
|
|
123
|
+
#listeners = new Map();
|
|
124
|
+
#maxEvents;
|
|
125
|
+
#logger;
|
|
126
|
+
#scoutContext;
|
|
127
|
+
#counters;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Object} options
|
|
131
|
+
* @param {number} options.maxEvents - Max events to retain in memory (default 5000)
|
|
132
|
+
* @param {Object} options.logger - External logger with info/warn/error methods (optional)
|
|
133
|
+
* @param {Object} options.scoutContext - Scout identity info attached to all events
|
|
134
|
+
*/
|
|
135
|
+
constructor(options = {}) {
|
|
136
|
+
this.#maxEvents = options.maxEvents || 5000;
|
|
137
|
+
this.#logger = options.logger || null;
|
|
138
|
+
this.#scoutContext = options.scoutContext || {};
|
|
139
|
+
this.#counters = {
|
|
140
|
+
sessions: { created: 0, committed: 0, cancelled: 0 },
|
|
141
|
+
offers: { received: 0, evaluated: 0, committed: 0 },
|
|
142
|
+
transactions: { created: 0, fulfilled: 0, timedOut: 0 },
|
|
143
|
+
ping: { total: 0, successful: 0, failed: 0 },
|
|
144
|
+
requests: { total: 0, succeeded: 0, failed: 0, totalDurationMs: 0 },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Record an activity event
|
|
150
|
+
*/
|
|
151
|
+
record(type, data = {}) {
|
|
152
|
+
const event = new ActivityEvent(type, data);
|
|
153
|
+
|
|
154
|
+
// Attach scout context
|
|
155
|
+
event.scoutId = this.#scoutContext.scoutId || null;
|
|
156
|
+
event.scoutExternalId = this.#scoutContext.externalId || null;
|
|
157
|
+
event.scoutName = this.#scoutContext.name || null;
|
|
158
|
+
|
|
159
|
+
// Store event
|
|
160
|
+
this.#events.push(event);
|
|
161
|
+
|
|
162
|
+
// Prune if over limit
|
|
163
|
+
if (this.#events.length > this.#maxEvents) {
|
|
164
|
+
this.#events = this.#events.slice(-Math.floor(this.#maxEvents * 0.8));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Update counters
|
|
168
|
+
this.#updateCounters(event);
|
|
169
|
+
|
|
170
|
+
// Emit to listeners
|
|
171
|
+
this.#emit(type, event);
|
|
172
|
+
|
|
173
|
+
// Forward to external logger if configured
|
|
174
|
+
if (this.#logger) {
|
|
175
|
+
const logData = { ...event.toJSON(), scout: this.#scoutContext };
|
|
176
|
+
if (event.error) {
|
|
177
|
+
this.#logger.error?.(logData, `[scout] ${type}`);
|
|
178
|
+
} else {
|
|
179
|
+
this.#logger.info?.(logData, `[scout] ${type}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return event;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a timer for measuring durations
|
|
188
|
+
* Returns a function that, when called, records the event with duration
|
|
189
|
+
*/
|
|
190
|
+
startTimer(type, data = {}) {
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
const correlationId = data.correlationId || randomUUID();
|
|
193
|
+
|
|
194
|
+
return (completionData = {}) => {
|
|
195
|
+
return this.record(type, {
|
|
196
|
+
...data,
|
|
197
|
+
...completionData,
|
|
198
|
+
correlationId,
|
|
199
|
+
durationMs: Date.now() - start,
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Subscribe to events by type
|
|
206
|
+
* Supports exact match ('offer.committed') or prefix ('offer.*')
|
|
207
|
+
*/
|
|
208
|
+
on(eventType, handler) {
|
|
209
|
+
if (!this.#listeners.has(eventType)) {
|
|
210
|
+
this.#listeners.set(eventType, []);
|
|
211
|
+
}
|
|
212
|
+
this.#listeners.get(eventType).push(handler);
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remove a listener
|
|
218
|
+
*/
|
|
219
|
+
off(eventType, handler) {
|
|
220
|
+
const handlers = this.#listeners.get(eventType);
|
|
221
|
+
if (handlers) {
|
|
222
|
+
const idx = handlers.indexOf(handler);
|
|
223
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
224
|
+
}
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Query events with optional filters
|
|
230
|
+
* @param {Object} filters
|
|
231
|
+
* @param {string} filters.type - Event type (exact or prefix with *)
|
|
232
|
+
* @param {string} filters.sessionId - Filter by session
|
|
233
|
+
* @param {string} filters.correlationId - Filter by correlation
|
|
234
|
+
* @param {string} filters.since - ISO timestamp, events after this time
|
|
235
|
+
* @param {number} filters.limit - Max results (default 100)
|
|
236
|
+
*/
|
|
237
|
+
getEvents(filters = {}) {
|
|
238
|
+
let results = this.#events;
|
|
239
|
+
|
|
240
|
+
if (filters.type) {
|
|
241
|
+
if (filters.type.endsWith('*')) {
|
|
242
|
+
const prefix = filters.type.slice(0, -1);
|
|
243
|
+
results = results.filter((e) => e.type.startsWith(prefix));
|
|
244
|
+
} else {
|
|
245
|
+
results = results.filter((e) => e.type === filters.type);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (filters.sessionId) {
|
|
250
|
+
results = results.filter((e) => e.sessionId === filters.sessionId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (filters.correlationId) {
|
|
254
|
+
results = results.filter((e) => e.correlationId === filters.correlationId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (filters.since) {
|
|
258
|
+
const sinceMs = new Date(filters.since).getTime();
|
|
259
|
+
results = results.filter((e) => e.epochMs >= sinceMs);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (filters.limit) {
|
|
263
|
+
results = results.slice(-filters.limit);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return results.map((e) => e.toJSON());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get aggregate summary counters
|
|
271
|
+
*/
|
|
272
|
+
getSummary() {
|
|
273
|
+
const avgRequestMs =
|
|
274
|
+
this.#counters.requests.total > 0
|
|
275
|
+
? Math.round(this.#counters.requests.totalDurationMs / this.#counters.requests.total)
|
|
276
|
+
: 0;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
scout: { ...this.#scoutContext },
|
|
280
|
+
sessions: { ...this.#counters.sessions },
|
|
281
|
+
offers: { ...this.#counters.offers },
|
|
282
|
+
transactions: { ...this.#counters.transactions },
|
|
283
|
+
ping: { ...this.#counters.ping },
|
|
284
|
+
requests: {
|
|
285
|
+
total: this.#counters.requests.total,
|
|
286
|
+
succeeded: this.#counters.requests.succeeded,
|
|
287
|
+
failed: this.#counters.requests.failed,
|
|
288
|
+
avgDurationMs: avgRequestMs,
|
|
289
|
+
},
|
|
290
|
+
eventsRecorded: this.#events.length,
|
|
291
|
+
oldestEvent: this.#events[0]?.timestamp || null,
|
|
292
|
+
newestEvent: this.#events[this.#events.length - 1]?.timestamp || null,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Clear all events (keeps counters)
|
|
298
|
+
*/
|
|
299
|
+
clearEvents() {
|
|
300
|
+
this.#events = [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Reset everything (events and counters)
|
|
305
|
+
*/
|
|
306
|
+
reset() {
|
|
307
|
+
this.#events = [];
|
|
308
|
+
this.#counters = {
|
|
309
|
+
sessions: { created: 0, committed: 0, cancelled: 0 },
|
|
310
|
+
offers: { received: 0, evaluated: 0, committed: 0 },
|
|
311
|
+
transactions: { created: 0, fulfilled: 0, timedOut: 0 },
|
|
312
|
+
ping: { total: 0, successful: 0, failed: 0 },
|
|
313
|
+
requests: { total: 0, succeeded: 0, failed: 0, totalDurationMs: 0 },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Update scout context (e.g., after registration when scoutId is assigned)
|
|
319
|
+
*/
|
|
320
|
+
setScoutContext(context) {
|
|
321
|
+
this.#scoutContext = { ...this.#scoutContext, ...context };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Generate a new correlation ID
|
|
326
|
+
*/
|
|
327
|
+
static correlationId() {
|
|
328
|
+
return randomUUID();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- Private methods ---
|
|
332
|
+
|
|
333
|
+
#updateCounters(event) {
|
|
334
|
+
switch (event.type) {
|
|
335
|
+
case ScoutActivityEventTypes.SESSION_CREATED:
|
|
336
|
+
this.#counters.sessions.created++;
|
|
337
|
+
break;
|
|
338
|
+
case ScoutActivityEventTypes.SESSION_COMMITTED:
|
|
339
|
+
this.#counters.sessions.committed++;
|
|
340
|
+
break;
|
|
341
|
+
case ScoutActivityEventTypes.SESSION_CANCELLED:
|
|
342
|
+
this.#counters.sessions.cancelled++;
|
|
343
|
+
break;
|
|
344
|
+
case ScoutActivityEventTypes.OFFERS_RECEIVED:
|
|
345
|
+
this.#counters.offers.received++;
|
|
346
|
+
break;
|
|
347
|
+
case ScoutActivityEventTypes.OFFERS_EVALUATED:
|
|
348
|
+
this.#counters.offers.evaluated++;
|
|
349
|
+
break;
|
|
350
|
+
case ScoutActivityEventTypes.OFFER_COMMITTED:
|
|
351
|
+
this.#counters.offers.committed++;
|
|
352
|
+
break;
|
|
353
|
+
case ScoutActivityEventTypes.TRANSACTION_CREATED:
|
|
354
|
+
this.#counters.transactions.created++;
|
|
355
|
+
break;
|
|
356
|
+
case ScoutActivityEventTypes.TRANSACTION_FULFILLED:
|
|
357
|
+
this.#counters.transactions.fulfilled++;
|
|
358
|
+
break;
|
|
359
|
+
case ScoutActivityEventTypes.TRANSACTION_FULFILLMENT_TIMEOUT:
|
|
360
|
+
this.#counters.transactions.timedOut++;
|
|
361
|
+
break;
|
|
362
|
+
case ScoutActivityEventTypes.PING_SUCCESS:
|
|
363
|
+
this.#counters.ping.total++;
|
|
364
|
+
this.#counters.ping.successful++;
|
|
365
|
+
break;
|
|
366
|
+
case ScoutActivityEventTypes.PING_FAILED:
|
|
367
|
+
this.#counters.ping.total++;
|
|
368
|
+
this.#counters.ping.failed++;
|
|
369
|
+
break;
|
|
370
|
+
case ScoutActivityEventTypes.REQUEST_COMPLETE:
|
|
371
|
+
this.#counters.requests.total++;
|
|
372
|
+
this.#counters.requests.succeeded++;
|
|
373
|
+
if (event.durationMs) this.#counters.requests.totalDurationMs += event.durationMs;
|
|
374
|
+
break;
|
|
375
|
+
case ScoutActivityEventTypes.REQUEST_FAILED:
|
|
376
|
+
this.#counters.requests.total++;
|
|
377
|
+
this.#counters.requests.failed++;
|
|
378
|
+
if (event.durationMs) this.#counters.requests.totalDurationMs += event.durationMs;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#emit(type, event) {
|
|
384
|
+
// Exact match listeners
|
|
385
|
+
const exact = this.#listeners.get(type) || [];
|
|
386
|
+
for (const handler of exact) {
|
|
387
|
+
try {
|
|
388
|
+
handler(event.toJSON());
|
|
389
|
+
} catch (_) {
|
|
390
|
+
// Listener errors must not break the Scout
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Wildcard listeners (e.g., 'offer.*' matches 'offer.committed')
|
|
395
|
+
for (const [pattern, handlers] of this.#listeners) {
|
|
396
|
+
if (pattern.endsWith('*')) {
|
|
397
|
+
const prefix = pattern.slice(0, -1);
|
|
398
|
+
if (type.startsWith(prefix) && pattern !== type) {
|
|
399
|
+
for (const handler of handlers) {
|
|
400
|
+
try {
|
|
401
|
+
handler(event.toJSON());
|
|
402
|
+
} catch (_) {
|
|
403
|
+
// Listener errors must not break the Scout
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Global wildcard
|
|
411
|
+
const global = this.#listeners.get('*') || [];
|
|
412
|
+
for (const handler of global) {
|
|
413
|
+
try {
|
|
414
|
+
handler(event.toJSON());
|
|
415
|
+
} catch (_) {
|
|
416
|
+
// Listener errors must not break the Scout
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|