@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/src/index.js ADDED
@@ -0,0 +1,655 @@
1
+ /**
2
+ * AURA Scout SDK
3
+ *
4
+ * Build buying agents that participate in agentic commerce.
5
+ * Scouts represent users in the AURA ecosystem, expressing intent
6
+ * and negotiating with Beacons through the neutral Core broker.
7
+ *
8
+ * @example
9
+ * ```js
10
+ * import { createScout } from '@aura-labs-ai/scout';
11
+ *
12
+ * // Zero config — auto-generates Ed25519 identity and registers with Core
13
+ * const scout = createScout();
14
+ * await scout.ready();
15
+ *
16
+ * // Express purchase intent with constraints
17
+ * const session = await scout.intent('I need 500 widgets', {
18
+ * maxBudget: 50000,
19
+ * deliveryBy: new Date('2026-03-01'),
20
+ * });
21
+ *
22
+ * // Wait for offers (polling)
23
+ * const offers = await session.waitForOffers();
24
+ *
25
+ * // Commit to best offer that meets constraints
26
+ * if (session.bestOffer) {
27
+ * const tx = await session.commit(session.bestOffer.id);
28
+ * console.log('Transaction:', tx.id);
29
+ * }
30
+ * ```
31
+ */
32
+
33
+ import { ScoutClient } from './client.js';
34
+ import { Session, Constraints } from './session.js';
35
+ import { KeyManager, MemoryStorage } from './key-manager.js';
36
+ import {
37
+ ScoutError,
38
+ ConnectionError,
39
+ AuthenticationError,
40
+ SessionError,
41
+ ConstraintError,
42
+ OfferError,
43
+ } from './errors.js';
44
+ import { ScoutActivityLogger, ScoutActivityEventTypes } from './activity.js';
45
+ import { IntentSession } from './intent-session.js';
46
+
47
+ // Storage adapters (re-exported from @aura-labs-ai/sdk-common)
48
+ import { FileStorage, KeychainStorage, createStorage } from '@aura-labs-ai/sdk-common';
49
+
50
+ // Protocol Integrations
51
+ import { MCPClient } from './mcp/client.js';
52
+ import { AP2Mandates, generateKeyPair as generateAP2KeyPair } from './ap2/mandates.js';
53
+ import { VisaTAP, TAPError } from './tap/visa.js';
54
+
55
+ /**
56
+ * Create a new Scout instance
57
+ *
58
+ * @param {ScoutConfig} config - Scout configuration
59
+ * @returns {Scout} Configured Scout instance
60
+ */
61
+ export function createScout(config) {
62
+ return new Scout(config);
63
+ }
64
+
65
+ /**
66
+ * Scout - User-sovereign buying agent
67
+ *
68
+ * The Scout represents a user's interests in the AURA commerce ecosystem.
69
+ * It handles:
70
+ * - Expressing purchase intent in natural language
71
+ * - Managing commerce sessions
72
+ * - Evaluating offers against user-defined constraints
73
+ * - Committing to transactions
74
+ */
75
+ export class Scout {
76
+ #client;
77
+ #config;
78
+ #keyManager;
79
+ #sessions = new Map();
80
+ #registered = false;
81
+ #agentId = null;
82
+ #readyPromise = null;
83
+ #activity;
84
+
85
+ /**
86
+ * @param {ScoutConfig} config
87
+ * @param {string} [config.apiKey] - Optional API key (legacy — for developer analytics/billing)
88
+ * @param {string} [config.coreUrl] - AURA Core API URL
89
+ * @param {number} [config.timeout] - Request timeout in ms
90
+ * @param {object} [config.storage] - Storage adapter with get/set/remove methods
91
+ * @param {object} [config.constraints] - Default constraints for all sessions
92
+ * @param {object} [config.logger] - External logger instance (optional)
93
+ */
94
+ constructor(config = {}) {
95
+ this.#config = {
96
+ coreUrl: 'https://aura-labsai-production.up.railway.app',
97
+ timeout: 30000,
98
+ constraints: {},
99
+ ...config,
100
+ };
101
+
102
+ // Initialize key manager with provided storage (or in-memory default)
103
+ this.#keyManager = new KeyManager({
104
+ storage: this.#config.storage,
105
+ });
106
+
107
+ // Initialize activity logger
108
+ this.#activity = new ScoutActivityLogger({
109
+ maxEvents: this.#config.maxActivityEvents || 5000,
110
+ logger: this.#config.logger || null,
111
+ scoutContext: {},
112
+ });
113
+
114
+ this.#client = new ScoutClient(this.#config, this.#activity);
115
+
116
+ this.#activity.record(ScoutActivityEventTypes.SCOUT_CREATED, {
117
+ metadata: {
118
+ coreUrl: this.#config.coreUrl,
119
+ timeout: this.#config.timeout,
120
+ },
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Initialize the Scout — generates keys and registers with AURA Core
126
+ *
127
+ * Call this once before using the Scout. It generates Ed25519 keys (if new)
128
+ * and registers with Core via POST /agents/register (if not already registered).
129
+ *
130
+ * This is idempotent — safe to call multiple times.
131
+ *
132
+ * @returns {Promise<Scout>} Returns self for chaining
133
+ */
134
+ async ready() {
135
+ // Deduplicate concurrent ready() calls
136
+ if (this.#readyPromise) return this.#readyPromise;
137
+
138
+ const finish = this.#activity.startTimer(ScoutActivityEventTypes.SCOUT_READY);
139
+
140
+ this.#readyPromise = this.#initialize();
141
+ try {
142
+ await this.#readyPromise;
143
+ finish({ success: true });
144
+ return this;
145
+ } catch (error) {
146
+ this.#readyPromise = null;
147
+ this.#activity.record(ScoutActivityEventTypes.SCOUT_READY_FAILED, {
148
+ success: false,
149
+ error: error.message,
150
+ });
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Read-only connectivity check against AURA Core
157
+ *
158
+ * Calls GET /health/ready to verify Core is reachable and its
159
+ * dependencies (database, redis) are healthy. Does NOT require
160
+ * prior ready() or register() calls. Does NOT send auth headers.
161
+ * Safe to call in a loop for monitoring.
162
+ *
163
+ * @returns {Promise<PingResponse>} Health status with latency
164
+ */
165
+ async ping() {
166
+ const start = performance.now();
167
+ const timestamp = new Date().toISOString();
168
+
169
+ try {
170
+ const url = `${this.#config.coreUrl}/health/ready`;
171
+ const response = await fetch(url, {
172
+ method: 'GET',
173
+ headers: {
174
+ 'X-Request-ID': crypto.randomUUID(),
175
+ 'X-Scout-SDK': '@aura-labs-ai/scout/0.1.0',
176
+ },
177
+ signal: AbortSignal.timeout(this.#config.timeout),
178
+ });
179
+
180
+ const latency_ms = Math.round(performance.now() - start);
181
+ const body = await response.json();
182
+
183
+ const result = {
184
+ status: response.ok ? 'ok' : 'degraded',
185
+ core: {
186
+ status: body.status,
187
+ checks: body.checks || {},
188
+ },
189
+ latency_ms,
190
+ timestamp,
191
+ };
192
+
193
+ this.#activity.record(ScoutActivityEventTypes.PING_SUCCESS, {
194
+ metadata: { latency_ms, coreStatus: body.status },
195
+ });
196
+
197
+ return result;
198
+ } catch (error) {
199
+ const latency_ms = Math.round(performance.now() - start);
200
+
201
+ const code =
202
+ error.name === 'TimeoutError' || error.name === 'AbortError'
203
+ ? 'CORE_TIMEOUT'
204
+ : 'CORE_UNREACHABLE';
205
+
206
+ const result = {
207
+ status: 'error',
208
+ code,
209
+ message: `Could not connect to AURA Core at ${this.#config.coreUrl}`,
210
+ latency_ms,
211
+ timestamp,
212
+ };
213
+
214
+ this.#activity.record(ScoutActivityEventTypes.PING_FAILED, {
215
+ metadata: { latency_ms, code, error: error.message },
216
+ });
217
+
218
+ return result;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Register this Scout with AURA Core
224
+ *
225
+ * Called automatically by ready(), but can be called explicitly
226
+ * to re-register or update metadata.
227
+ *
228
+ * Uses /agents/register with Ed25519 proof-of-possession.
229
+ * The legacy /scouts/register endpoint has been removed (DEC-015).
230
+ *
231
+ * @param {object} metadata - Optional metadata about this Scout
232
+ * @returns {Promise<{agentId: string}>}
233
+ */
234
+ async register(metadata = {}) {
235
+ if (this.#registered && this.#agentId) {
236
+ return { agentId: this.#agentId };
237
+ }
238
+
239
+ const finish = this.#activity.startTimer(ScoutActivityEventTypes.SCOUT_REGISTERED);
240
+
241
+ try {
242
+ // Ensure keys are initialized
243
+ if (!this.#keyManager.isInitialized) {
244
+ await this.#keyManager.init();
245
+ }
246
+
247
+ const publicKey = this.#keyManager.publicKey;
248
+
249
+ // Build registration body
250
+ const body = {
251
+ publicKey,
252
+ type: 'scout',
253
+ manifest: {
254
+ name: metadata.name || 'AURA Scout',
255
+ version: '0.1.0',
256
+ platform: metadata.platform || 'node-sdk',
257
+ sdkVersion: '0.1.0',
258
+ capabilities: ['intent', 'compare', 'mandate.sign'],
259
+ protocolVersions: ['ap2-v1', 'tap-v1'],
260
+ ...metadata,
261
+ },
262
+ };
263
+
264
+ // Sign the body for proof-of-possession
265
+ const bodyString = JSON.stringify(body);
266
+ const signature = this.#keyManager.sign(bodyString);
267
+
268
+ const result = await this.#client.postSigned('/agents/register', body, {
269
+ 'X-Agent-Signature': signature,
270
+ });
271
+
272
+ this.#agentId = result.agentId;
273
+ this.#registered = true;
274
+
275
+ // Persist agentId for next launch
276
+ await this.#keyManager.setAgentId(this.#agentId);
277
+
278
+ // Configure client to sign subsequent requests
279
+ this.#client.setKeyManager(this.#keyManager, this.#agentId);
280
+
281
+ // Update activity context with assigned agentId
282
+ this.#activity.setScoutContext({
283
+ agentId: this.#agentId,
284
+ });
285
+
286
+ finish({
287
+ success: true,
288
+ metadata: {
289
+ agentId: this.#agentId,
290
+ },
291
+ });
292
+
293
+ return { agentId: this.#agentId };
294
+ } catch (error) {
295
+ this.#activity.record(ScoutActivityEventTypes.SCOUT_REGISTRATION_FAILED, {
296
+ success: false,
297
+ error: error.message,
298
+ });
299
+ throw error;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Express purchase intent and create a commerce session
305
+ *
306
+ * Intent is expressed in natural language. AURA Core interprets the
307
+ * intent and matches it against Beacon capabilities.
308
+ *
309
+ * @param {string} intent - Natural language description of what you want
310
+ * @param {IntentOptions} options - Constraints and preferences
311
+ * @returns {Promise<Session>}
312
+ *
313
+ * @example
314
+ * ```js
315
+ * // Simple intent
316
+ * const session = await scout.intent('I want to buy a laptop');
317
+ *
318
+ * // Intent with constraints
319
+ * const session = await scout.intent('I need office supplies', {
320
+ * maxBudget: 500,
321
+ * deliveryBy: new Date('2026-02-15'),
322
+ * hardConstraints: [
323
+ * { field: 'sustainable', operator: 'eq', value: true }
324
+ * ],
325
+ * softPreferences: [
326
+ * { field: 'rating', operator: 'gte', value: 4.5, weight: 10 }
327
+ * ]
328
+ * });
329
+ * ```
330
+ */
331
+ async intent(intent, options = {}) {
332
+ // Auto-initialize if needed
333
+ if (!this.#registered) {
334
+ await this.ready();
335
+ }
336
+
337
+ const finish = this.#activity.startTimer(ScoutActivityEventTypes.INTENT_CREATED, {
338
+ metadata: { intentText: intent.substring(0, 100) },
339
+ });
340
+
341
+ try {
342
+ // Merge session-specific constraints with defaults
343
+ const constraints = {
344
+ ...this.#config.constraints,
345
+ ...options,
346
+ };
347
+
348
+ // Agent identity is conveyed via Ed25519 signature headers, not the body.
349
+ // The server extracts agent ID from the verified X-Agent-Id header.
350
+ const response = await this.#client.post('/sessions', {
351
+ intent,
352
+ constraints: {
353
+ maxBudget: constraints.maxBudget,
354
+ deliveryBy: constraints.deliveryBy,
355
+ hardConstraints: constraints.hardConstraints,
356
+ softPreferences: constraints.softPreferences,
357
+ },
358
+ });
359
+
360
+ const sessionConfig = {
361
+ ...this.#config,
362
+ constraints,
363
+ activityLogger: this.#activity,
364
+ };
365
+
366
+ const session = new Session(response, this.#client, sessionConfig);
367
+ this.#sessions.set(session.id, session);
368
+
369
+ finish({
370
+ success: true,
371
+ metadata: {
372
+ sessionId: session.id,
373
+ },
374
+ });
375
+
376
+ return session;
377
+ } catch (error) {
378
+ this.#activity.record(ScoutActivityEventTypes.INTENT_FAILED, {
379
+ success: false,
380
+ error: error.message,
381
+ metadata: { intentText: intent.substring(0, 100) },
382
+ });
383
+ throw error;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Create a conversational intent session for completeness checking.
389
+ *
390
+ * The IntentSession accumulates text across multiple submit() calls,
391
+ * checks completeness via @aura-labs-ai/nlp, and generates clarification
392
+ * questions. When complete, call scout.intent() with the full text
393
+ * and the completeness attestation.
394
+ *
395
+ * This is additive — the existing scout.intent() API is unchanged.
396
+ *
397
+ * @param {Object} [options]
398
+ * @param {Object} [options.provider] — Optional LLM provider for model-based detection
399
+ * @returns {IntentSession}
400
+ *
401
+ * @example
402
+ * ```js
403
+ * const session = scout.createIntentSession();
404
+ *
405
+ * let result = await session.submit('I need keyboards');
406
+ * // result.complete === false, result.question === "How many would you like?"
407
+ *
408
+ * result = await session.submit('50 units under $5000');
409
+ * // result.complete === true
410
+ *
411
+ * // Now submit to Core with completeness attestation
412
+ * await scout.intent(session.getText(), {
413
+ * completeness: session.getAttestation(),
414
+ * });
415
+ * ```
416
+ */
417
+ createIntentSession(options = {}) {
418
+ return new IntentSession({
419
+ ...options,
420
+ activityLogger: this.#activity,
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Resume an existing session by ID
426
+ *
427
+ * @param {string} sessionId
428
+ * @returns {Promise<Session>}
429
+ */
430
+ async resumeSession(sessionId) {
431
+ if (this.#sessions.has(sessionId)) {
432
+ return this.#sessions.get(sessionId);
433
+ }
434
+
435
+ const finish = this.#activity.startTimer(ScoutActivityEventTypes.SESSION_RESUMED, {
436
+ sessionId,
437
+ });
438
+
439
+ try {
440
+ const response = await this.#client.get(`/sessions/${sessionId}`);
441
+ const sessionConfig = {
442
+ ...this.#config,
443
+ activityLogger: this.#activity,
444
+ };
445
+ const session = new Session(response, this.#client, sessionConfig);
446
+ this.#sessions.set(session.id, session);
447
+
448
+ finish({
449
+ success: true,
450
+ sessionId,
451
+ });
452
+
453
+ return session;
454
+ } catch (error) {
455
+ this.#activity.record(ScoutActivityEventTypes.SESSION_RESUME_FAILED, {
456
+ sessionId,
457
+ success: false,
458
+ error: error.message,
459
+ });
460
+ throw error;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * List active sessions
466
+ *
467
+ * @returns {Session[]}
468
+ */
469
+ get activeSessions() {
470
+ return Array.from(this.#sessions.values()).filter(s => s.isActive);
471
+ }
472
+
473
+ /**
474
+ * Set default constraints for all sessions
475
+ *
476
+ * @param {object} constraints
477
+ */
478
+ setDefaultConstraints(constraints) {
479
+ this.#config.constraints = {
480
+ ...this.#config.constraints,
481
+ ...constraints,
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Get Scout's registration status
487
+ */
488
+ get isRegistered() {
489
+ return this.#registered;
490
+ }
491
+
492
+ /**
493
+ * Get Agent ID (null if not registered)
494
+ */
495
+ get id() {
496
+ return this.#agentId;
497
+ }
498
+
499
+ /**
500
+ * Get the agent's public key (base64-encoded Ed25519)
501
+ */
502
+ get publicKey() {
503
+ return this.#keyManager.publicKey;
504
+ }
505
+
506
+ /**
507
+ * Get Core URL
508
+ */
509
+ get coreUrl() {
510
+ return this.#client.coreUrl;
511
+ }
512
+
513
+ /**
514
+ * Access the activity logger for observability
515
+ *
516
+ * @example
517
+ * ```js
518
+ * // Subscribe to events
519
+ * scout.activity.on('intent.created', (event) => {
520
+ * console.log(`Intent created in ${event.durationMs}ms`);
521
+ * });
522
+ *
523
+ * // Get summary stats
524
+ * const stats = scout.activity.getSummary();
525
+ *
526
+ * // Query recent errors
527
+ * const errors = scout.activity.getEvents({ type: '*.error' });
528
+ * ```
529
+ */
530
+ get activity() {
531
+ return this.#activity;
532
+ }
533
+
534
+ // ─── Private ────────────────────────────────────────────────────────
535
+
536
+ async #initialize() {
537
+ // Step 1: Initialize key manager (generate or load keys)
538
+ const { isNew } = await this.#keyManager.init();
539
+
540
+ // Step 2: Check if already registered (agentId in storage)
541
+ const storedAgentId = await this.#keyManager.getAgentId();
542
+ if (storedAgentId) {
543
+ this.#agentId = storedAgentId;
544
+ this.#registered = true;
545
+ this.#client.setKeyManager(this.#keyManager, this.#agentId);
546
+ return;
547
+ }
548
+
549
+ // Step 3: Register with Core
550
+ await this.register();
551
+ }
552
+ }
553
+
554
+ // Re-export classes
555
+ export { Session, Constraints } from './session.js';
556
+ export { KeyManager, MemoryStorage } from './key-manager.js';
557
+ export { FileStorage, KeychainStorage, createStorage };
558
+ export {
559
+ ScoutError,
560
+ ConnectionError,
561
+ AuthenticationError,
562
+ SessionError,
563
+ ConstraintError,
564
+ OfferError,
565
+ } from './errors.js';
566
+ export { ScoutActivityLogger, ScoutActivityEventTypes, ActivityEvent } from './activity.js';
567
+ export { IntentSession } from './intent-session.js';
568
+
569
+ // =========================================================================
570
+ // Protocol Integrations
571
+ // =========================================================================
572
+
573
+ /**
574
+ * MCP (Model Context Protocol) Client
575
+ *
576
+ * Connect to MCP servers to access external tools and context.
577
+ *
578
+ * @example
579
+ * ```js
580
+ * import { MCPClient } from '@aura-labs-ai/scout';
581
+ *
582
+ * const mcp = new MCPClient();
583
+ * await mcp.connect('https://mcp.example.com/sse');
584
+ * const tools = await mcp.listAllTools();
585
+ * const result = await mcp.callTool('server-uri', 'search', { query: 'laptops' });
586
+ * ```
587
+ */
588
+ export { MCPClient };
589
+
590
+ /**
591
+ * AP2 Mandates - Google's Agent Payments Protocol
592
+ *
593
+ * Create user-signed mandates that authorize agent actions:
594
+ * - Intent Mandate: Authorizes shopping within constraints
595
+ * - Cart Mandate: Authorizes specific purchase
596
+ * - Payment Mandate: Authorization for payment networks
597
+ *
598
+ * @example
599
+ * ```js
600
+ * import { AP2Mandates, generateAP2KeyPair } from '@aura-labs-ai/scout';
601
+ *
602
+ * const { publicKey, privateKey } = generateAP2KeyPair();
603
+ *
604
+ * const intentMandate = await AP2Mandates.createIntent({
605
+ * agentId: 'scout-123',
606
+ * userId: 'user-456',
607
+ * userKey: privateKey,
608
+ * constraints: {
609
+ * maxAmount: 5000,
610
+ * currency: 'USD',
611
+ * categories: ['electronics'],
612
+ * validUntil: '2026-03-01T00:00:00Z',
613
+ * },
614
+ * });
615
+ * ```
616
+ */
617
+ export { AP2Mandates, generateAP2KeyPair };
618
+
619
+ /**
620
+ * Visa TAP - Trusted Agent Protocol
621
+ *
622
+ * Agent identity verification for commerce transactions.
623
+ * Allows payment networks to verify agent authenticity.
624
+ *
625
+ * @example
626
+ * ```js
627
+ * import { VisaTAP } from '@aura-labs-ai/scout';
628
+ *
629
+ * // Generate keys and register agent
630
+ * const keyPair = VisaTAP.generateKeyPair();
631
+ * const registration = await VisaTAP.register({
632
+ * agentId: 'my-scout-agent',
633
+ * publicKey: keyPair.publicKey,
634
+ * metadata: { name: 'Shopping Agent', operator: 'AURA Labs' },
635
+ * });
636
+ *
637
+ * // Sign HTTP requests for TAP verification
638
+ * const signedRequest = await VisaTAP.signRequest(
639
+ * { method: 'POST', url: '/api/pay', body: { amount: 100 } },
640
+ * { tapId: registration.tapId, privateKey: keyPair.privateKey, keyId: keyPair.keyId }
641
+ * );
642
+ * ```
643
+ */
644
+ export { VisaTAP, TAPError };
645
+
646
+ // Default export
647
+ export default {
648
+ createScout,
649
+ Scout,
650
+ KeyManager,
651
+ // Protocols
652
+ MCPClient,
653
+ AP2Mandates,
654
+ VisaTAP,
655
+ };