@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,366 @@
1
+ /**
2
+ * AP2 Mandates for AURA Scout SDK
3
+ *
4
+ * Implements Google's Agent Payments Protocol (AP2) mandate system
5
+ * for secure, auditable AI agent payments.
6
+ *
7
+ * Mandate Types:
8
+ * - Intent Mandate: User authorizes agent to shop within constraints
9
+ * - Cart Mandate: User authorizes specific purchase
10
+ * - Payment Mandate: Authorization for payment network
11
+ *
12
+ * @see https://ap2-protocol.org/specification/
13
+ */
14
+
15
+ import { createHash, createSign, createVerify, generateKeyPairSync } from 'crypto';
16
+
17
+ /**
18
+ * AP2 Mandates - Create and validate payment mandates
19
+ */
20
+ export class AP2Mandates {
21
+ /**
22
+ * Create an Intent Mandate
23
+ *
24
+ * Intent mandates authorize an agent to shop on behalf of a user
25
+ * within specified constraints (amount, categories, time, etc.)
26
+ *
27
+ * @param {Object} params
28
+ * @param {string} params.agentId - ID of the authorized agent
29
+ * @param {Object} params.constraints - Shopping constraints
30
+ * @param {number} params.constraints.maxAmount - Maximum purchase amount
31
+ * @param {string} params.constraints.currency - Currency code (USD, EUR, etc.)
32
+ * @param {string[]} params.constraints.categories - Allowed product categories
33
+ * @param {string} params.constraints.validUntil - ISO date string
34
+ * @param {string} params.constraints.merchantAllowlist - Optional merchant IDs
35
+ * @param {Object} params.userKey - User's signing key (private key)
36
+ * @returns {Object} Signed intent mandate
37
+ */
38
+ static async createIntent({
39
+ agentId,
40
+ constraints,
41
+ userKey,
42
+ userId,
43
+ metadata = {},
44
+ }) {
45
+ const mandate = {
46
+ type: 'intent',
47
+ version: '1.0',
48
+ id: generateMandateId(),
49
+ issuedAt: new Date().toISOString(),
50
+ issuer: {
51
+ type: 'user',
52
+ id: userId,
53
+ },
54
+ subject: {
55
+ type: 'agent',
56
+ id: agentId,
57
+ },
58
+ constraints: {
59
+ maxAmount: constraints.maxAmount,
60
+ currency: constraints.currency || 'USD',
61
+ categories: constraints.categories || [],
62
+ validUntil: constraints.validUntil,
63
+ validFrom: constraints.validFrom || new Date().toISOString(),
64
+ merchantAllowlist: constraints.merchantAllowlist || null,
65
+ merchantBlocklist: constraints.merchantBlocklist || null,
66
+ requireUserPresent: constraints.requireUserPresent ?? false,
67
+ maxTransactions: constraints.maxTransactions || null,
68
+ },
69
+ metadata,
70
+ };
71
+
72
+ // Create canonical form for signing
73
+ const canonical = canonicalize(mandate);
74
+ const signature = await sign(canonical, userKey);
75
+
76
+ return {
77
+ ...mandate,
78
+ proof: {
79
+ type: 'Ed25519Signature2020',
80
+ created: new Date().toISOString(),
81
+ verificationMethod: `did:key:${userId}#keys-1`,
82
+ proofPurpose: 'assertionMethod',
83
+ proofValue: signature,
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Create a Cart Mandate
90
+ *
91
+ * Cart mandates authorize a specific purchase (exact items, price).
92
+ * Created when user explicitly approves a particular offer.
93
+ *
94
+ * @param {Object} params
95
+ * @param {string} params.sessionId - Commerce session ID
96
+ * @param {Object} params.offer - The specific offer being authorized
97
+ * @param {Object} params.userKey - User's signing key
98
+ * @returns {Object} Signed cart mandate
99
+ */
100
+ static async createCart({
101
+ sessionId,
102
+ offer,
103
+ userKey,
104
+ userId,
105
+ intentMandateId,
106
+ metadata = {},
107
+ }) {
108
+ const mandate = {
109
+ type: 'cart',
110
+ version: '1.0',
111
+ id: generateMandateId(),
112
+ issuedAt: new Date().toISOString(),
113
+ issuer: {
114
+ type: 'user',
115
+ id: userId,
116
+ },
117
+ intentMandateRef: intentMandateId,
118
+ cart: {
119
+ sessionId,
120
+ offerId: offer.id,
121
+ merchantId: offer.beaconId,
122
+ merchantName: offer.beaconName,
123
+ items: [{
124
+ product: offer.product,
125
+ quantity: offer.quantity,
126
+ unitPrice: offer.unitPrice,
127
+ currency: offer.currency || 'USD',
128
+ }],
129
+ totalAmount: offer.totalPrice || (offer.unitPrice * offer.quantity),
130
+ currency: offer.currency || 'USD',
131
+ deliveryDate: offer.deliveryDate,
132
+ },
133
+ userPresent: true, // User explicitly approved
134
+ expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30 min
135
+ metadata,
136
+ };
137
+
138
+ const canonical = canonicalize(mandate);
139
+ const signature = await sign(canonical, userKey);
140
+
141
+ return {
142
+ ...mandate,
143
+ proof: {
144
+ type: 'Ed25519Signature2020',
145
+ created: new Date().toISOString(),
146
+ verificationMethod: `did:key:${userId}#keys-1`,
147
+ proofPurpose: 'assertionMethod',
148
+ proofValue: signature,
149
+ },
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Create a Payment Mandate
155
+ *
156
+ * Payment mandates are shared with the payment network to provide
157
+ * context about the AI agent transaction.
158
+ *
159
+ * @param {Object} params
160
+ * @param {Object} params.cartMandate - The cart mandate being paid
161
+ * @param {Object} params.paymentMethod - Payment method details
162
+ * @param {Object} params.agentKey - Agent's signing key
163
+ * @returns {Object} Signed payment mandate
164
+ */
165
+ static async createPayment({
166
+ cartMandate,
167
+ paymentMethod,
168
+ agentId,
169
+ agentKey,
170
+ tapCredentials = null,
171
+ }) {
172
+ const mandate = {
173
+ type: 'payment',
174
+ version: '1.0',
175
+ id: generateMandateId(),
176
+ issuedAt: new Date().toISOString(),
177
+ cartMandateRef: cartMandate.id,
178
+ agent: {
179
+ id: agentId,
180
+ tapId: tapCredentials?.tapId || null, // Visa TAP registration
181
+ },
182
+ transaction: {
183
+ amount: cartMandate.cart.totalAmount,
184
+ currency: cartMandate.cart.currency,
185
+ merchantId: cartMandate.cart.merchantId,
186
+ merchantName: cartMandate.cart.merchantName,
187
+ },
188
+ userPresent: cartMandate.userPresent,
189
+ paymentMethod: {
190
+ type: paymentMethod.type, // 'card', 'bank', 'crypto'
191
+ network: paymentMethod.network, // 'visa', 'mastercard', etc.
192
+ tokenized: true, // Always use tokens, never raw card numbers
193
+ },
194
+ riskSignals: {
195
+ userAuthTime: cartMandate.issuedAt,
196
+ agentSessionDuration: calculateSessionDuration(cartMandate),
197
+ intentMandatePresent: !!cartMandate.intentMandateRef,
198
+ },
199
+ };
200
+
201
+ const canonical = canonicalize(mandate);
202
+ const signature = await sign(canonical, agentKey);
203
+
204
+ return {
205
+ ...mandate,
206
+ proof: {
207
+ type: 'Ed25519Signature2020',
208
+ created: new Date().toISOString(),
209
+ verificationMethod: `did:agent:${agentId}#keys-1`,
210
+ proofPurpose: 'assertionMethod',
211
+ proofValue: signature,
212
+ },
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Verify a mandate's signature
218
+ */
219
+ static async verify(mandate, publicKey) {
220
+ if (!mandate.proof?.proofValue) {
221
+ return { valid: false, error: 'No proof present' };
222
+ }
223
+
224
+ const proof = mandate.proof;
225
+ const mandateWithoutProof = { ...mandate };
226
+ delete mandateWithoutProof.proof;
227
+
228
+ const canonical = canonicalize(mandateWithoutProof);
229
+
230
+ try {
231
+ const isValid = await verifySignature(canonical, proof.proofValue, publicKey);
232
+ return { valid: isValid };
233
+ } catch (error) {
234
+ return { valid: false, error: error.message };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Check if an intent mandate covers a proposed purchase
240
+ */
241
+ static validateIntentCoverage(intentMandate, proposedPurchase) {
242
+ const c = intentMandate.constraints;
243
+ const errors = [];
244
+
245
+ // Check amount
246
+ if (proposedPurchase.totalAmount > c.maxAmount) {
247
+ errors.push(`Amount ${proposedPurchase.totalAmount} exceeds max ${c.maxAmount}`);
248
+ }
249
+
250
+ // Check currency
251
+ if (c.currency && proposedPurchase.currency !== c.currency) {
252
+ errors.push(`Currency ${proposedPurchase.currency} not allowed (expected ${c.currency})`);
253
+ }
254
+
255
+ // Check validity period
256
+ const now = new Date();
257
+ if (c.validFrom && new Date(c.validFrom) > now) {
258
+ errors.push('Mandate not yet valid');
259
+ }
260
+ if (c.validUntil && new Date(c.validUntil) < now) {
261
+ errors.push('Mandate expired');
262
+ }
263
+
264
+ // Check categories
265
+ if (c.categories?.length > 0 && proposedPurchase.category) {
266
+ if (!c.categories.includes(proposedPurchase.category)) {
267
+ errors.push(`Category ${proposedPurchase.category} not in allowed list`);
268
+ }
269
+ }
270
+
271
+ // Check merchant allowlist
272
+ if (c.merchantAllowlist?.length > 0) {
273
+ if (!c.merchantAllowlist.includes(proposedPurchase.merchantId)) {
274
+ errors.push('Merchant not in allowlist');
275
+ }
276
+ }
277
+
278
+ // Check merchant blocklist
279
+ if (c.merchantBlocklist?.length > 0) {
280
+ if (c.merchantBlocklist.includes(proposedPurchase.merchantId)) {
281
+ errors.push('Merchant is blocklisted');
282
+ }
283
+ }
284
+
285
+ return {
286
+ valid: errors.length === 0,
287
+ errors,
288
+ };
289
+ }
290
+ }
291
+
292
+ // =============================================================================
293
+ // Helper Functions
294
+ // =============================================================================
295
+
296
+ /**
297
+ * Generate unique mandate ID
298
+ */
299
+ function generateMandateId() {
300
+ const timestamp = Date.now().toString(36);
301
+ const random = Math.random().toString(36).substring(2, 10);
302
+ return `mandate_${timestamp}_${random}`;
303
+ }
304
+
305
+ /**
306
+ * Canonicalize object for signing (deterministic JSON)
307
+ */
308
+ function canonicalize(obj) {
309
+ return JSON.stringify(obj, Object.keys(obj).sort());
310
+ }
311
+
312
+ /**
313
+ * Sign data with private key
314
+ */
315
+ async function sign(data, privateKey) {
316
+ // In production, use proper Ed25519 signing
317
+ // For now, use Node.js crypto
318
+ if (typeof privateKey === 'string') {
319
+ // Assume PEM format
320
+ const signer = createSign('sha256');
321
+ signer.update(data);
322
+ return signer.sign(privateKey, 'base64');
323
+ }
324
+
325
+ // Mock signing for development
326
+ const hash = createHash('sha256').update(data).digest('base64');
327
+ return `mock_signature_${hash.substring(0, 20)}`;
328
+ }
329
+
330
+ /**
331
+ * Verify signature
332
+ */
333
+ async function verifySignature(data, signature, publicKey) {
334
+ if (signature.startsWith('mock_signature_')) {
335
+ // Mock verification for development
336
+ const hash = createHash('sha256').update(data).digest('base64');
337
+ return signature === `mock_signature_${hash.substring(0, 20)}`;
338
+ }
339
+
340
+ // Real verification
341
+ const verifier = createVerify('sha256');
342
+ verifier.update(data);
343
+ return verifier.verify(publicKey, signature, 'base64');
344
+ }
345
+
346
+ /**
347
+ * Calculate session duration from mandate timestamps
348
+ */
349
+ function calculateSessionDuration(cartMandate) {
350
+ const cartTime = new Date(cartMandate.issuedAt).getTime();
351
+ const now = Date.now();
352
+ return Math.floor((now - cartTime) / 1000); // seconds
353
+ }
354
+
355
+ /**
356
+ * Generate a key pair for signing (development utility)
357
+ */
358
+ export function generateKeyPair() {
359
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
360
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
361
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
362
+ });
363
+ return { publicKey, privateKey };
364
+ }
365
+
366
+ export default AP2Mandates;
package/src/client.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Scout HTTP Client
3
+ *
4
+ * Handles all HTTP communication with AURA Core.
5
+ *
6
+ * Supports two authentication modes:
7
+ * 1. Ed25519 signed requests (new — via KeyManager)
8
+ * 2. Bearer token (legacy — via apiKey)
9
+ *
10
+ * When a KeyManager is set, all requests are signed with the agent's
11
+ * private key and include X-Agent-Id, X-Agent-Signature, and
12
+ * X-Agent-Timestamp headers.
13
+ *
14
+ * Adds correlation IDs (X-Request-ID) to every request for end-to-end tracing.
15
+ * Reports request timing and outcomes to the activity logger when provided.
16
+ */
17
+
18
+ import { randomUUID } from 'crypto';
19
+ import { ConnectionError, AuthenticationError, ScoutError } from './errors.js';
20
+ import { ScoutActivityEventTypes } from './activity.js';
21
+
22
+ // API version prefix — all requests target this version of the Core API.
23
+ // Bump this constant when upgrading to a new API version.
24
+ const API_VERSION = '/v1';
25
+
26
+ export class ScoutClient {
27
+ #config;
28
+ #keyManager = null;
29
+ #agentId = null;
30
+ #activityLogger;
31
+
32
+ constructor(config, activityLogger = null) {
33
+ this.#config = config;
34
+ this.#activityLogger = activityLogger;
35
+ }
36
+
37
+ /**
38
+ * Configure the client to sign requests with Ed25519
39
+ *
40
+ * @param {KeyManager} keyManager - Initialized KeyManager instance
41
+ * @param {string} agentId - Registered agent UUID
42
+ */
43
+ setKeyManager(keyManager, agentId) {
44
+ this.#keyManager = keyManager;
45
+ this.#agentId = agentId;
46
+ }
47
+
48
+ /**
49
+ * Make HTTP GET request to Core
50
+ */
51
+ async get(path) {
52
+ return this.#request('GET', path);
53
+ }
54
+
55
+ /**
56
+ * Make HTTP POST request to Core
57
+ */
58
+ async post(path, body) {
59
+ return this.#request('POST', path, body);
60
+ }
61
+
62
+ /**
63
+ * Make HTTP POST request with explicit extra headers (used for registration)
64
+ *
65
+ * @param {string} path - API path
66
+ * @param {object} body - Request body
67
+ * @param {object} extraHeaders - Additional headers (e.g., X-Agent-Signature for registration)
68
+ */
69
+ async postSigned(path, body, extraHeaders = {}) {
70
+ return this.#request('POST', path, body, extraHeaders);
71
+ }
72
+
73
+ /**
74
+ * Make HTTP PUT request to Core
75
+ */
76
+ async put(path, body) {
77
+ return this.#request('PUT', path, body);
78
+ }
79
+
80
+ /**
81
+ * Make HTTP DELETE request to Core
82
+ */
83
+ async delete(path) {
84
+ return this.#request('DELETE', path);
85
+ }
86
+
87
+ /**
88
+ * Core HTTP request method
89
+ *
90
+ * If a KeyManager is configured, requests are signed with Ed25519.
91
+ * Falls back to Bearer token auth if apiKey is provided.
92
+ */
93
+ async #request(method, path, body = null, extraHeaders = {}) {
94
+ const url = `${this.#config.coreUrl}${API_VERSION}${path}`;
95
+ const requestId = randomUUID();
96
+ const bodyString = body ? JSON.stringify(body) : null;
97
+
98
+ const headers = {
99
+ 'Content-Type': 'application/json',
100
+ 'X-Scout-SDK': '@aura-labs-ai/scout/0.1.0',
101
+ 'X-Request-ID': requestId,
102
+ ...extraHeaders,
103
+ };
104
+
105
+ // Auth: prefer Ed25519 signing, fall back to Bearer token
106
+ if (this.#keyManager && this.#agentId) {
107
+ const { signature, timestamp } = this.#keyManager.signRequest({
108
+ method,
109
+ path,
110
+ body: bodyString,
111
+ });
112
+ headers['X-Agent-Id'] = this.#agentId;
113
+ headers['X-Agent-Signature'] = signature;
114
+ headers['X-Agent-Timestamp'] = timestamp;
115
+ } else if (this.#config.apiKey) {
116
+ headers['Authorization'] = `Bearer ${this.#config.apiKey}`;
117
+ }
118
+
119
+ const options = {
120
+ method,
121
+ headers,
122
+ signal: AbortSignal.timeout(this.#config.timeout),
123
+ };
124
+
125
+ if (bodyString) {
126
+ options.body = bodyString;
127
+ }
128
+
129
+ // Record request start
130
+ let completeTimer;
131
+ if (this.#activityLogger) {
132
+ completeTimer = this.#activityLogger.startTimer(ScoutActivityEventTypes.REQUEST_COMPLETE, {
133
+ correlationId: requestId,
134
+ metadata: { method, path, url },
135
+ });
136
+ this.#activityLogger.record(ScoutActivityEventTypes.REQUEST_START, {
137
+ correlationId: requestId,
138
+ metadata: { method, path, url },
139
+ });
140
+ }
141
+
142
+ try {
143
+ const response = await fetch(url, options);
144
+
145
+ if (!response.ok) {
146
+ const error = await response.json().catch(() => ({}));
147
+ const errorMsg = error.message || `Request failed with status ${response.status}`;
148
+
149
+ // Record failure
150
+ if (this.#activityLogger) {
151
+ this.#activityLogger.record(ScoutActivityEventTypes.REQUEST_FAILED, {
152
+ correlationId: requestId,
153
+ success: false,
154
+ error: errorMsg,
155
+ metadata: { method, path, statusCode: response.status },
156
+ });
157
+ }
158
+
159
+ if (response.status === 401) {
160
+ throw new AuthenticationError(errorMsg);
161
+ }
162
+
163
+ if (response.status === 403) {
164
+ throw new AuthenticationError(errorMsg);
165
+ }
166
+
167
+ if (response.status === 404) {
168
+ throw new ScoutError(errorMsg, 'NOT_FOUND');
169
+ }
170
+
171
+ throw new ScoutError(errorMsg, error.code || 'REQUEST_FAILED');
172
+ }
173
+
174
+ const result = await response.json();
175
+
176
+ // Record success
177
+ if (completeTimer) {
178
+ completeTimer({
179
+ success: true,
180
+ metadata: { method, path, statusCode: response.status },
181
+ });
182
+ }
183
+
184
+ return result;
185
+ } catch (error) {
186
+ if (error instanceof ScoutError) throw error;
187
+
188
+ const isTimeout = error.name === 'AbortError' || error.name === 'TimeoutError';
189
+ const errorMsg = isTimeout
190
+ ? `Request timed out after ${this.#config.timeout}ms`
191
+ : `Failed to connect to AURA Core: ${error.message}`;
192
+
193
+ // Record failure
194
+ if (this.#activityLogger) {
195
+ this.#activityLogger.record(ScoutActivityEventTypes.REQUEST_FAILED, {
196
+ correlationId: requestId,
197
+ success: false,
198
+ error: errorMsg,
199
+ metadata: { method, path, timeout: isTimeout },
200
+ });
201
+ }
202
+
203
+ if (isTimeout) {
204
+ throw new ConnectionError(errorMsg);
205
+ }
206
+
207
+ throw new ConnectionError(errorMsg);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Get the configured Core URL
213
+ */
214
+ get coreUrl() {
215
+ return this.#config.coreUrl;
216
+ }
217
+ }
package/src/errors.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Scout SDK Error Classes
3
+ */
4
+
5
+ /**
6
+ * Base error for all Scout SDK errors
7
+ */
8
+ export class ScoutError extends Error {
9
+ constructor(message, code = 'SCOUT_ERROR') {
10
+ super(message);
11
+ this.name = 'ScoutError';
12
+ this.code = code;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Connection/network errors
18
+ */
19
+ export class ConnectionError extends ScoutError {
20
+ constructor(message) {
21
+ super(message, 'CONNECTION_ERROR');
22
+ this.name = 'ConnectionError';
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Authentication errors (invalid API key, etc.)
28
+ */
29
+ export class AuthenticationError extends ScoutError {
30
+ constructor(message) {
31
+ super(message, 'AUTHENTICATION_ERROR');
32
+ this.name = 'AuthenticationError';
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Session-related errors
38
+ */
39
+ export class SessionError extends ScoutError {
40
+ constructor(message, code = 'SESSION_ERROR') {
41
+ super(message, code);
42
+ this.name = 'SessionError';
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Constraint validation errors
48
+ */
49
+ export class ConstraintError extends ScoutError {
50
+ constructor(message, constraint) {
51
+ super(message, 'CONSTRAINT_ERROR');
52
+ this.name = 'ConstraintError';
53
+ this.constraint = constraint;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Offer-related errors
59
+ */
60
+ export class OfferError extends ScoutError {
61
+ constructor(message, offerId) {
62
+ super(message, 'OFFER_ERROR');
63
+ this.name = 'OfferError';
64
+ this.offerId = offerId;
65
+ }
66
+ }