@haven_ai/sdk 0.1.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/dist/index.js ADDED
@@ -0,0 +1,636 @@
1
+ import { ethers } from 'ethers';
2
+
3
+ // src/signer.ts
4
+
5
+ // src/types.ts
6
+ var HavenError = class extends Error {
7
+ constructor(message, code, statusCode, paymentId) {
8
+ super(message);
9
+ this.code = code;
10
+ this.statusCode = statusCode;
11
+ this.paymentId = paymentId;
12
+ this.name = "HavenError";
13
+ }
14
+ code;
15
+ statusCode;
16
+ paymentId;
17
+ };
18
+ var HavenApiError = class extends HavenError {
19
+ constructor(message, statusCode, body) {
20
+ super(message, "API_ERROR", statusCode);
21
+ this.body = body;
22
+ this.name = "HavenApiError";
23
+ }
24
+ body;
25
+ };
26
+ var HavenSigningError = class extends HavenError {
27
+ constructor(message) {
28
+ super(message, "SIGNING_ERROR");
29
+ this.name = "HavenSigningError";
30
+ }
31
+ };
32
+ var HavenTimeoutError = class extends HavenError {
33
+ constructor(paymentId) {
34
+ super(
35
+ `Timed out waiting for payment ${paymentId} to confirm`,
36
+ "TIMEOUT",
37
+ void 0,
38
+ paymentId
39
+ );
40
+ this.name = "HavenTimeoutError";
41
+ }
42
+ };
43
+
44
+ // src/signer.ts
45
+ function signHash(privateKey, hash) {
46
+ try {
47
+ const signingKey = new ethers.SigningKey(privateKey);
48
+ const sig = signingKey.sign(hash);
49
+ return sig.serialized;
50
+ } catch (err) {
51
+ throw new HavenSigningError(
52
+ `Failed to sign hash: ${err instanceof Error ? err.message : String(err)}`
53
+ );
54
+ }
55
+ }
56
+ function addressFromKey(privateKey) {
57
+ try {
58
+ return new ethers.Wallet(privateKey).address;
59
+ } catch (err) {
60
+ throw new HavenSigningError(
61
+ `Invalid private key: ${err instanceof Error ? err.message : String(err)}`
62
+ );
63
+ }
64
+ }
65
+ function verifySignature(hash, signature, expectedAddress) {
66
+ try {
67
+ const recovered = ethers.recoverAddress(hash, signature);
68
+ return recovered.toLowerCase() === expectedAddress.toLowerCase();
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // src/x402.ts
75
+ var SUPPORTED_X402_NETWORKS = {
76
+ "eip155:100": "Gnosis Chain",
77
+ "eip155:8453": "Base"
78
+ };
79
+ var GNOSIS_TOKENS = {
80
+ "0x0000000000000000000000000000000000000000": { symbol: "xDAI", decimals: 18 },
81
+ "0xcb444e90d8198415266c6a2724b7900fb12fc56e": { symbol: "EURe", decimals: 18 },
82
+ "0x2a22f9c3b484c3629090feed35f17ff8f88f76f0": { symbol: "USDC.e", decimals: 6 }
83
+ };
84
+ var BASE_TOKENS = {
85
+ "0x0000000000000000000000000000000000000000": { symbol: "ETH", decimals: 18 },
86
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6 }
87
+ };
88
+ var NETWORK_TOKENS = {
89
+ "eip155:100": GNOSIS_TOKENS,
90
+ "eip155:8453": BASE_TOKENS
91
+ };
92
+ function parsePaymentRequired(response) {
93
+ const v2Header = response.headers.get("PAYMENT-REQUIRED");
94
+ if (v2Header) {
95
+ try {
96
+ return JSON.parse(atob(v2Header));
97
+ } catch {
98
+ throw new Error("Failed to decode PAYMENT-REQUIRED header");
99
+ }
100
+ }
101
+ const v1Header = response.headers.get("X-PAYMENT");
102
+ if (v1Header) {
103
+ try {
104
+ return JSON.parse(atob(v1Header));
105
+ } catch {
106
+ throw new Error("Failed to decode X-PAYMENT header");
107
+ }
108
+ }
109
+ throw new Error(
110
+ "No x402 payment headers found in 402 response. Expected PAYMENT-REQUIRED (v2) or X-PAYMENT (v1) header."
111
+ );
112
+ }
113
+ function selectPaymentOption(accepts) {
114
+ if (!accepts || accepts.length === 0) return null;
115
+ for (const opt of accepts) {
116
+ if (opt.network in SUPPORTED_X402_NETWORKS) {
117
+ const networkTokens = NETWORK_TOKENS[opt.network];
118
+ if (networkTokens?.[opt.asset.toLowerCase()]) return opt;
119
+ }
120
+ }
121
+ for (const opt of accepts) {
122
+ if (opt.network in SUPPORTED_X402_NETWORKS) {
123
+ return opt;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ function encodePaymentProof(receipt) {
129
+ const payload = {
130
+ x402Version: 2,
131
+ payload: {
132
+ txHash: receipt.txHash,
133
+ paymentId: receipt.paymentId,
134
+ settledVia: "haven"
135
+ }
136
+ };
137
+ return btoa(JSON.stringify(payload));
138
+ }
139
+
140
+ // src/client.ts
141
+ var DEFAULT_BASE_URL = "http://localhost:3001";
142
+ var CHAIN_EXPLORER_TX = {
143
+ 100: "https://gnosisscan.io/tx",
144
+ 8453: "https://basescan.org/tx"
145
+ };
146
+ function buildExplorerUrl(chainId, txHash) {
147
+ const base = CHAIN_EXPLORER_TX[chainId ?? 100] ?? CHAIN_EXPLORER_TX[100];
148
+ return `${base}/${txHash}`;
149
+ }
150
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
151
+ var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
152
+ var DEFAULT_POLLING_INTERVAL = 3e3;
153
+ var HavenClient = class {
154
+ apiKey;
155
+ delegateKey;
156
+ baseUrl;
157
+ requestTimeout;
158
+ confirmationTimeout;
159
+ pollingInterval;
160
+ /** Delegate address derived from the private key (if provided) */
161
+ delegateAddress;
162
+ constructor(config) {
163
+ this.apiKey = config.apiKey;
164
+ this.delegateKey = config.delegateKey;
165
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
166
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
167
+ this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
168
+ this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
169
+ if (this.delegateKey) {
170
+ this.delegateAddress = addressFromKey(this.delegateKey);
171
+ }
172
+ }
173
+ // ── High-Level API ───────────────────────────────────────────────
174
+ /**
175
+ * Send a payment in one call.
176
+ *
177
+ * Creates the intent, signs the hash, submits the signature,
178
+ * and polls until confirmed (or throws on failure/timeout).
179
+ *
180
+ * Requires `delegateKey` to be set in the client config.
181
+ */
182
+ async pay(request) {
183
+ if (!this.delegateKey) {
184
+ throw new HavenSigningError(
185
+ "Cannot use pay() without a delegateKey. Use createIntent() + submitSignature() for manual signing."
186
+ );
187
+ }
188
+ const intent = await this.createIntent(request);
189
+ const signature = this.sign(intent.signData.hash);
190
+ await this.submitSignature(intent.paymentId, signature);
191
+ return this.waitForConfirmation(intent.paymentId);
192
+ }
193
+ // ── Step-by-Step API ─────────────────────────────────────────────
194
+ /**
195
+ * Step 1: Create a payment intent.
196
+ *
197
+ * Returns the intent with the hash to sign.
198
+ */
199
+ async createIntent(request) {
200
+ const raw = await this.post("/payments", {
201
+ token: request.token,
202
+ amount: request.amount,
203
+ to: request.to
204
+ });
205
+ return {
206
+ paymentId: raw.payment_id,
207
+ status: "pending_signature",
208
+ expiresAt: raw.expires_at,
209
+ signData: raw.sign_data
210
+ };
211
+ }
212
+ /**
213
+ * Step 2: Sign a hash with the delegate key.
214
+ *
215
+ * Returns the 65-byte signature (0x-prefixed).
216
+ * Requires `delegateKey` to be set in the client config.
217
+ */
218
+ sign(hash) {
219
+ if (!this.delegateKey) {
220
+ throw new HavenSigningError(
221
+ "Cannot sign without a delegateKey. Pass the private key in HavenClient config, or sign externally."
222
+ );
223
+ }
224
+ const signature = signHash(this.delegateKey, hash);
225
+ if (!verifySignature(hash, signature, this.delegateAddress)) {
226
+ throw new HavenSigningError(
227
+ "Local signature verification failed \u2014 recovered address does not match delegate key."
228
+ );
229
+ }
230
+ return signature;
231
+ }
232
+ /**
233
+ * Step 3: Submit a signature to execute the payment.
234
+ *
235
+ * The signature can come from `client.sign()` or from external signing.
236
+ */
237
+ async submitSignature(paymentId, signature) {
238
+ const raw = await this.post(
239
+ `/payments/${paymentId}/sign`,
240
+ { signature }
241
+ );
242
+ return {
243
+ status: raw.status,
244
+ txHash: raw.tx_hash
245
+ };
246
+ }
247
+ /**
248
+ * Get the current status of a payment.
249
+ */
250
+ async getPayment(paymentId) {
251
+ const raw = await this.get(`/payments/${paymentId}`);
252
+ return this.mapPaymentResult(raw);
253
+ }
254
+ /**
255
+ * Poll until a payment reaches a terminal status (confirmed, failed, expired).
256
+ */
257
+ async waitForConfirmation(paymentId) {
258
+ const deadline = Date.now() + this.confirmationTimeout;
259
+ while (Date.now() < deadline) {
260
+ const result = await this.getPayment(paymentId);
261
+ if (result.status === "confirmed" || result.status === "failed" || result.status === "expired") {
262
+ return result;
263
+ }
264
+ await sleep(this.pollingInterval);
265
+ }
266
+ throw new HavenTimeoutError(paymentId);
267
+ }
268
+ // ── x402 Protocol Support ────────────────────────────────────────
269
+ /**
270
+ * Authorize an x402 payment.
271
+ *
272
+ * Takes the parsed PaymentRequired from a 402 response, selects a
273
+ * compatible payment option, signs and executes the payment through Haven.
274
+ *
275
+ * Requires `delegateKey` to be set in the client config.
276
+ */
277
+ async authorizeX402(paymentRequired) {
278
+ if (!this.delegateKey) {
279
+ throw new HavenSigningError(
280
+ "delegateKey is required for x402 payments. Pass it in the HavenClient config."
281
+ );
282
+ }
283
+ const option = selectPaymentOption(paymentRequired.accepts);
284
+ if (!option) {
285
+ throw new HavenApiError(
286
+ "No compatible payment option found in x402 requirements. Haven supports Gnosis Chain (eip155:100) and Base (eip155:8453).",
287
+ 400
288
+ );
289
+ }
290
+ const raw = await this.post("/x402", {
291
+ url: paymentRequired.resource.url,
292
+ payTo: option.payTo,
293
+ amount: option.amount,
294
+ asset: option.asset,
295
+ network: option.network,
296
+ description: paymentRequired.resource.description
297
+ });
298
+ if (raw.success && raw.tx_hash) {
299
+ return {
300
+ success: true,
301
+ paymentId: raw.payment_id,
302
+ txHash: raw.tx_hash,
303
+ token: raw.token ?? "",
304
+ amount: raw.amount ?? "",
305
+ to: raw.to ?? "",
306
+ resourceUrl: paymentRequired.resource.url,
307
+ explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : "")
308
+ };
309
+ }
310
+ if (!raw.sign_data?.hash) {
311
+ throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
312
+ }
313
+ const sig = signHash(this.delegateKey, raw.sign_data.hash);
314
+ const execResult = await this.post(
315
+ `/payments/${raw.payment_id}/sign`,
316
+ { signature: sig }
317
+ );
318
+ if (execResult.status !== "confirmed") {
319
+ throw new HavenApiError(
320
+ execResult.error ?? `x402 payment ${execResult.status}`,
321
+ 502,
322
+ execResult
323
+ );
324
+ }
325
+ return {
326
+ success: true,
327
+ paymentId: raw.payment_id,
328
+ txHash: execResult.tx_hash ?? "",
329
+ token: execResult.token ?? raw.token ?? "",
330
+ amount: execResult.amount ?? raw.amount ?? "",
331
+ to: execResult.to ?? raw.to ?? "",
332
+ resourceUrl: paymentRequired.resource.url,
333
+ explorerUrl: execResult.explorer_url ?? (execResult.tx_hash ? buildExplorerUrl(execResult.chain_id, execResult.tx_hash) : "")
334
+ };
335
+ }
336
+ /**
337
+ * Fetch wrapper that automatically handles HTTP 402 responses.
338
+ *
339
+ * Works like the standard `fetch()` but intercepts 402 responses,
340
+ * pays via x402 through Haven, and retries the request.
341
+ *
342
+ * ```ts
343
+ * const response = await haven.fetch('https://paid-api.com/data')
344
+ * const data = await response.json()
345
+ * ```
346
+ *
347
+ * Requires `delegateKey` to be set in the client config.
348
+ */
349
+ async fetch(url, init) {
350
+ const response = await globalThis.fetch(url, init);
351
+ if (response.status !== 402) return response;
352
+ let paymentRequired;
353
+ try {
354
+ paymentRequired = parsePaymentRequired(response);
355
+ } catch {
356
+ return response;
357
+ }
358
+ const receipt = await this.authorizeX402(paymentRequired);
359
+ const retryHeaders = new Headers(init?.headers);
360
+ retryHeaders.set("PAYMENT-SIGNATURE", encodePaymentProof(receipt));
361
+ return globalThis.fetch(url, {
362
+ ...init,
363
+ headers: retryHeaders
364
+ });
365
+ }
366
+ // ── Tool Execution (for agent frameworks) ────────────────────────
367
+ /**
368
+ * Execute a tool call by name and input.
369
+ *
370
+ * Designed to plug directly into agent tool-call handlers:
371
+ *
372
+ * ```ts
373
+ * if (block.type === 'tool_use') {
374
+ * const result = await haven.executeTool(block.name, block.input)
375
+ * // send result back to the model
376
+ * }
377
+ * ```
378
+ */
379
+ async executeTool(toolName, input) {
380
+ if (toolName === "make_payment") {
381
+ const { token, amount, to } = input;
382
+ try {
383
+ const result = await this.pay({ token, amount, to });
384
+ return {
385
+ success: result.status === "confirmed",
386
+ payment_id: result.paymentId,
387
+ status: result.status,
388
+ tx_hash: result.txHash,
389
+ token: result.token,
390
+ amount: result.amount,
391
+ to: result.to,
392
+ explorer_url: result.explorerUrl,
393
+ error: result.errorMessage
394
+ };
395
+ } catch (err) {
396
+ return {
397
+ success: false,
398
+ error: err instanceof Error ? err.message : String(err)
399
+ };
400
+ }
401
+ }
402
+ if (toolName === "authorize_x402_payment") {
403
+ const { url, payTo, amount, asset, network, description } = input;
404
+ try {
405
+ const receipt = await this.authorizeX402({
406
+ x402Version: 2,
407
+ resource: { url, description },
408
+ accepts: [
409
+ {
410
+ scheme: "exact",
411
+ network,
412
+ amount,
413
+ asset,
414
+ payTo,
415
+ maxTimeoutSeconds: 30
416
+ }
417
+ ]
418
+ });
419
+ return {
420
+ success: true,
421
+ payment_id: receipt.paymentId,
422
+ tx_hash: receipt.txHash,
423
+ token: receipt.token,
424
+ amount: receipt.amount,
425
+ to: receipt.to,
426
+ resource_url: receipt.resourceUrl,
427
+ explorer_url: receipt.explorerUrl
428
+ };
429
+ } catch (err) {
430
+ return {
431
+ success: false,
432
+ error: err instanceof Error ? err.message : String(err)
433
+ };
434
+ }
435
+ }
436
+ if (toolName === "get_payment_status") {
437
+ const { payment_id } = input;
438
+ const result = await this.getPayment(payment_id);
439
+ return {
440
+ payment_id: result.paymentId,
441
+ status: result.status,
442
+ tx_hash: result.txHash,
443
+ token: result.token,
444
+ amount: result.amount,
445
+ to: result.to,
446
+ explorer_url: result.explorerUrl
447
+ };
448
+ }
449
+ throw new Error(`Unknown tool: ${toolName}`);
450
+ }
451
+ // ── HTTP Helpers ─────────────────────────────────────────────────
452
+ async post(path, body) {
453
+ return this.request("POST", path, body);
454
+ }
455
+ async get(path) {
456
+ return this.request("GET", path);
457
+ }
458
+ async request(method, path, body) {
459
+ const url = `${this.baseUrl}${path}`;
460
+ const controller = new AbortController();
461
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
462
+ try {
463
+ const res = await fetch(url, {
464
+ method,
465
+ headers: {
466
+ "Content-Type": "application/json",
467
+ "Authorization": `Bearer ${this.apiKey}`
468
+ },
469
+ body: body ? JSON.stringify(body) : void 0,
470
+ signal: controller.signal
471
+ });
472
+ const data = await res.json();
473
+ if (!res.ok) {
474
+ const message = data.error ?? data.details ?? `API request failed`;
475
+ throw new HavenApiError(message, res.status, data);
476
+ }
477
+ return data;
478
+ } catch (err) {
479
+ if (err instanceof HavenApiError) throw err;
480
+ if (err instanceof Error && err.name === "AbortError") {
481
+ throw new HavenApiError(`Request to ${path} timed out`, 408);
482
+ }
483
+ throw new HavenApiError(
484
+ `Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
485
+ 0
486
+ );
487
+ } finally {
488
+ clearTimeout(timeout);
489
+ }
490
+ }
491
+ // ── Mapping Helpers ──────────────────────────────────────────────
492
+ mapPaymentResult(raw) {
493
+ return {
494
+ paymentId: raw.payment_id,
495
+ status: raw.status,
496
+ token: raw.token,
497
+ amount: raw.amount,
498
+ to: raw.to,
499
+ txHash: raw.tx_hash,
500
+ errorMessage: raw.error_message,
501
+ explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : null),
502
+ createdAt: raw.created_at,
503
+ signedAt: raw.signed_at,
504
+ submittedAt: raw.submitted_at,
505
+ confirmedAt: raw.confirmed_at,
506
+ expiresAt: raw.expires_at
507
+ };
508
+ }
509
+ };
510
+ function sleep(ms) {
511
+ return new Promise((resolve) => setTimeout(resolve, ms));
512
+ }
513
+
514
+ // src/tools.ts
515
+ var makePaymentSchema = {
516
+ type: "object",
517
+ properties: {
518
+ token: {
519
+ type: "string",
520
+ description: "Token to send. Gnosis Chain: EURe, USDC.e, xDAI. Base: USDC, ETH."
521
+ },
522
+ amount: {
523
+ type: "string",
524
+ description: 'Amount to send as a decimal string, e.g. "5.00"'
525
+ },
526
+ to: {
527
+ type: "string",
528
+ description: "Recipient Ethereum address (0x...)"
529
+ },
530
+ reason: {
531
+ type: "string",
532
+ description: "Brief reason for this payment (for audit trail)"
533
+ }
534
+ },
535
+ required: ["token", "amount", "to", "reason"]
536
+ };
537
+ var getPaymentStatusSchema = {
538
+ type: "object",
539
+ properties: {
540
+ payment_id: {
541
+ type: "string",
542
+ description: "The payment ID returned from make_payment"
543
+ }
544
+ },
545
+ required: ["payment_id"]
546
+ };
547
+ var authorizeX402Schema = {
548
+ type: "object",
549
+ properties: {
550
+ url: {
551
+ type: "string",
552
+ description: "The URL that returned HTTP 402"
553
+ },
554
+ payTo: {
555
+ type: "string",
556
+ description: "Payment recipient address from the 402 response"
557
+ },
558
+ amount: {
559
+ type: "string",
560
+ description: 'Payment amount in atomic units (e.g. "1000000" for 1 USDC)'
561
+ },
562
+ asset: {
563
+ type: "string",
564
+ description: "Token contract address from the 402 response"
565
+ },
566
+ network: {
567
+ type: "string",
568
+ description: 'CAIP-2 chain ID. "eip155:100" for Gnosis Chain, "eip155:8453" for Base.'
569
+ },
570
+ description: {
571
+ type: "string",
572
+ description: "Description of the resource being paid for"
573
+ }
574
+ },
575
+ required: ["url", "payTo", "amount", "asset", "network"]
576
+ };
577
+ var MAKE_PAYMENT_DESCRIPTION = "Send a payment from the Haven-managed Safe wallet. The payment will be validated against the agent's on-chain spending policy. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
578
+ var GET_STATUS_DESCRIPTION = "Check the status of a previously initiated payment. Returns the current status, transaction hash (if confirmed), and payment details.";
579
+ var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns 402 with x402 payment requirements, use this tool to pay and get access. Haven evaluates the payment against policy and executes from the Safe wallet.";
580
+ function claudeTools() {
581
+ return [
582
+ {
583
+ name: "make_payment",
584
+ description: MAKE_PAYMENT_DESCRIPTION,
585
+ input_schema: makePaymentSchema
586
+ },
587
+ {
588
+ name: "get_payment_status",
589
+ description: GET_STATUS_DESCRIPTION,
590
+ input_schema: getPaymentStatusSchema
591
+ },
592
+ {
593
+ name: "authorize_x402_payment",
594
+ description: AUTHORIZE_X402_DESCRIPTION,
595
+ input_schema: authorizeX402Schema
596
+ }
597
+ ];
598
+ }
599
+ function openaiTools() {
600
+ return [
601
+ {
602
+ type: "function",
603
+ function: {
604
+ name: "make_payment",
605
+ description: MAKE_PAYMENT_DESCRIPTION,
606
+ parameters: makePaymentSchema
607
+ }
608
+ },
609
+ {
610
+ type: "function",
611
+ function: {
612
+ name: "get_payment_status",
613
+ description: GET_STATUS_DESCRIPTION,
614
+ parameters: getPaymentStatusSchema
615
+ }
616
+ },
617
+ {
618
+ type: "function",
619
+ function: {
620
+ name: "authorize_x402_payment",
621
+ description: AUTHORIZE_X402_DESCRIPTION,
622
+ parameters: authorizeX402Schema
623
+ }
624
+ }
625
+ ];
626
+ }
627
+ var havenTools = {
628
+ /** Tool definitions in Anthropic/Claude format */
629
+ claude: claudeTools,
630
+ /** Tool definitions in OpenAI function-calling format */
631
+ openai: openaiTools
632
+ };
633
+
634
+ export { HavenApiError, HavenClient, HavenError, HavenSigningError, HavenTimeoutError, addressFromKey, encodePaymentProof, havenTools, parsePaymentRequired, selectPaymentOption, signHash, verifySignature };
635
+ //# sourceMappingURL=index.js.map
636
+ //# sourceMappingURL=index.js.map