@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
|
@@ -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
|
+
}
|