@toon-protocol/client 0.9.2 → 0.10.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 CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-WHAEQLIW.js";
7
7
 
8
8
  // src/ToonClient.ts
9
- import { generateSecretKey as generateSecretKey3, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
9
+ import { generateSecretKey as generateSecretKey3, getPublicKey as getPublicKey2, finalizeEvent } from "nostr-tools/pure";
10
10
 
11
11
  // src/config.ts
12
12
  import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
@@ -21,6 +21,7 @@ var ToonClientError = class extends Error {
21
21
  this.code = code;
22
22
  this.name = "ToonClientError";
23
23
  }
24
+ code;
24
25
  };
25
26
  var NetworkError = class extends ToonClientError {
26
27
  constructor(message, cause) {
@@ -1352,6 +1353,232 @@ var BtpRuntimeClient = class {
1352
1353
  }
1353
1354
  };
1354
1355
 
1356
+ // src/adapters/HttpIlpClient.ts
1357
+ var ILP_CLAIM_HEADER = "ILP-Payment-Channel-Claim";
1358
+ var ILP_CLAIM_WRAPPED_HEADER = "ILP-Payment-Channel-Claim-Wrapped";
1359
+ var ILP_PEER_ID_HEADER = "ILP-Peer-Id";
1360
+ var HttpIlpClient = class {
1361
+ httpEndpoint;
1362
+ peerId;
1363
+ authToken;
1364
+ timeout;
1365
+ retryConfig;
1366
+ httpClient;
1367
+ createWebSocket;
1368
+ constructor(config) {
1369
+ this.httpEndpoint = config.httpEndpoint;
1370
+ this.peerId = config.peerId;
1371
+ this.authToken = config.authToken;
1372
+ this.timeout = config.timeout ?? 3e4;
1373
+ this.retryConfig = {
1374
+ maxRetries: config.maxRetries ?? 3,
1375
+ retryDelay: config.retryDelay ?? 1e3
1376
+ };
1377
+ this.httpClient = config.httpClient ?? fetch;
1378
+ this.createWebSocket = config.createWebSocket;
1379
+ }
1380
+ /**
1381
+ * Send an ILP PREPARE via `POST /ilp` WITHOUT a claim. The connector accepts
1382
+ * this only on free/zero-amount routes; paid writes must use
1383
+ * {@link sendIlpPacketWithClaim}. Satisfies the IlpClient interface.
1384
+ */
1385
+ async sendIlpPacket(params) {
1386
+ return withRetry(() => this.postPrepare(params), {
1387
+ maxRetries: this.retryConfig.maxRetries,
1388
+ retryDelay: this.retryConfig.retryDelay,
1389
+ exponentialBackoff: true,
1390
+ shouldRetry: (error) => error instanceof NetworkError
1391
+ });
1392
+ }
1393
+ /**
1394
+ * Send an ILP PREPARE via `POST /ilp` with the payment-channel claim attached
1395
+ * as the `ILP-Payment-Channel-Claim` header. `claim` is the SAME JSON object
1396
+ * the BTP path attaches as the `payment-channel-claim` protocolData entry —
1397
+ * we base64(JSON.stringify(claim)) it, byte-for-byte identical to BTP.
1398
+ */
1399
+ async sendIlpPacketWithClaim(params, claim) {
1400
+ return withRetry(() => this.postPrepare(params, claim), {
1401
+ maxRetries: this.retryConfig.maxRetries,
1402
+ retryDelay: this.retryConfig.retryDelay,
1403
+ exponentialBackoff: true,
1404
+ shouldRetry: (error) => error instanceof NetworkError
1405
+ });
1406
+ }
1407
+ /**
1408
+ * Upgrade to a duplex BTP session over the SAME endpoint.
1409
+ *
1410
+ * Derives the `ws(s)://` URL from `httpEndpoint`, opens a WebSocket with
1411
+ * `Sec-WebSocket-Protocol: btp` and the same `ILP-Peer-Id` + `Authorization`
1412
+ * headers, and returns a connected {@link BtpRuntimeClient}. When auth headers
1413
+ * are present the connector pre-authenticates the session (no in-band auth
1414
+ * frame); without them the BtpRuntimeClient falls back to the normal BTP
1415
+ * auth-frame flow.
1416
+ *
1417
+ * NOTE: passing per-connection headers + a subprotocol to a WebSocket is
1418
+ * Node-only (the `ws` package). Browsers cannot set arbitrary request headers
1419
+ * on a WebSocket handshake, so a browser consumer must use the gateway
1420
+ * transport or BTP-with-auth-frame instead.
1421
+ */
1422
+ async upgradeToBtp() {
1423
+ const btpUrl = httpEndpointToBtpUrl(this.httpEndpoint);
1424
+ const createWebSocket = this.createWebSocket ?? await makeBtpWebSocketFactory(this.authHeaders());
1425
+ const client = new BtpRuntimeClient({
1426
+ btpUrl,
1427
+ // BtpRuntimeClient sends an auth frame using these; when the connector
1428
+ // pre-authenticated via Upgrade headers it accepts the (redundant) frame.
1429
+ peerId: this.peerId ?? "client",
1430
+ authToken: this.authToken ?? "",
1431
+ createWebSocket
1432
+ });
1433
+ await client.connect();
1434
+ return client;
1435
+ }
1436
+ // ─── Private ──────────────────────────────────────────────────────────────
1437
+ authHeaders() {
1438
+ const headers = {};
1439
+ if (this.peerId) headers[ILP_PEER_ID_HEADER] = this.peerId;
1440
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1441
+ return headers;
1442
+ }
1443
+ /**
1444
+ * Single attempt: serialize the PREPARE, POST it, and map the response.
1445
+ * @throws {NetworkError} On connection/timeout failures (retried).
1446
+ * @throws {ConnectorError} On non-retryable transport errors (5xx / unexpected).
1447
+ */
1448
+ async postPrepare(params, claim) {
1449
+ const requestTimeout = params.timeout ?? this.timeout;
1450
+ const prepare = serializeIlpPrepare({
1451
+ type: ILPPacketType.PREPARE,
1452
+ amount: BigInt(params.amount),
1453
+ destination: params.destination,
1454
+ executionCondition: new Uint8Array(32),
1455
+ expiresAt: new Date(Date.now() + requestTimeout),
1456
+ data: fromBase64(params.data)
1457
+ });
1458
+ const headers = {
1459
+ "Content-Type": "application/octet-stream",
1460
+ ...this.authHeaders()
1461
+ };
1462
+ if (claim !== void 0) {
1463
+ headers[ILP_CLAIM_HEADER] = toBase64(
1464
+ encodeUtf8(JSON.stringify(claim))
1465
+ );
1466
+ }
1467
+ const controller = new AbortController();
1468
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
1469
+ try {
1470
+ const response = await this.httpClient(this.httpEndpoint, {
1471
+ method: "POST",
1472
+ headers,
1473
+ // Copy into a fresh ArrayBuffer so fetch sees a clean body, not a view.
1474
+ body: prepare.slice(),
1475
+ signal: controller.signal
1476
+ });
1477
+ clearTimeout(timeoutId);
1478
+ return await this.mapResponse(response);
1479
+ } catch (error) {
1480
+ clearTimeout(timeoutId);
1481
+ throw this.mapTransportError(error, requestTimeout);
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Map a `200 OK` body (OER FULFILL/REJECT) to an IlpSendResult; map a non-2xx
1486
+ * to a transport error. Per the wire contract, ILP-level rejects arrive as a
1487
+ * 200 + REJECT body — only HTTP non-2xx means a transport-layer failure.
1488
+ */
1489
+ async mapResponse(response) {
1490
+ if (response.ok) {
1491
+ const buf = new Uint8Array(await response.arrayBuffer());
1492
+ if (buf.length === 0) {
1493
+ throw new ConnectorError("Empty 200 body from /ilp (expected OER ILP response)");
1494
+ }
1495
+ const ilp = deserializeIlpPacket(buf);
1496
+ if (ilp.type === ILPPacketType.FULFILL) {
1497
+ return {
1498
+ accepted: true,
1499
+ data: ilp.data.length > 0 ? toBase64(ilp.data) : void 0
1500
+ };
1501
+ }
1502
+ return {
1503
+ accepted: false,
1504
+ code: ilp.code,
1505
+ message: ilp.message,
1506
+ data: ilp.data.length > 0 ? toBase64(ilp.data) : void 0
1507
+ };
1508
+ }
1509
+ const body = await response.text().catch(() => "");
1510
+ const detail = body ? `: ${body}` : "";
1511
+ if (response.status >= 500) {
1512
+ throw new ConnectorError(
1513
+ `Connector transport error (${response.status} ${response.statusText})${detail}`
1514
+ );
1515
+ }
1516
+ throw new ConnectorError(
1517
+ `ILP-over-HTTP request rejected (${response.status} ${response.statusText})${detail}`
1518
+ );
1519
+ }
1520
+ mapTransportError(error, requestTimeout) {
1521
+ if (error instanceof ConnectorError || error instanceof NetworkError) {
1522
+ return error;
1523
+ }
1524
+ if (error instanceof Error && error.name === "AbortError") {
1525
+ return new NetworkError(`Request timeout after ${requestTimeout}ms`, error);
1526
+ }
1527
+ if (error instanceof TypeError && (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED") || error.message.includes("ECONNRESET") || error.message.includes("ETIMEDOUT") || error.message.includes("network"))) {
1528
+ return new NetworkError(`Network connection failed: ${error.message}`, error);
1529
+ }
1530
+ return new ConnectorError(
1531
+ `Unexpected error during ILP-over-HTTP request: ${error instanceof Error ? error.message : String(error)}`,
1532
+ error instanceof Error ? error : void 0
1533
+ );
1534
+ }
1535
+ };
1536
+ function httpEndpointToBtpUrl(httpEndpoint) {
1537
+ return httpEndpoint.replace(/^https:\/\//i, "wss://").replace(/^http:\/\//i, "ws://");
1538
+ }
1539
+ async function makeBtpWebSocketFactory(headers) {
1540
+ const { createRequire } = await import("module");
1541
+ const require2 = createRequire(import.meta.url);
1542
+ const WS = require2("ws");
1543
+ const ws = WS;
1544
+ const WSClass = typeof ws === "function" ? ws : typeof ws.default === "function" ? ws.default : typeof ws.WebSocket === "function" ? ws.WebSocket : null;
1545
+ if (WSClass === null) {
1546
+ throw new Error(
1547
+ "makeBtpWebSocketFactory: require('ws') did not yield a constructor on .default, .WebSocket, or the module root."
1548
+ );
1549
+ }
1550
+ return (url) => (
1551
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1552
+ new WSClass(url, "btp", { headers })
1553
+ );
1554
+ }
1555
+
1556
+ // src/adapters/selectIlpTransport.ts
1557
+ function readDiscoveredIlpPeer(peer) {
1558
+ const p = peer ?? {};
1559
+ return {
1560
+ btpEndpoint: typeof p["btpEndpoint"] === "string" ? p["btpEndpoint"] : void 0,
1561
+ httpEndpoint: typeof p["httpEndpoint"] === "string" ? p["httpEndpoint"] : void 0,
1562
+ supportsUpgrade: typeof p["supportsUpgrade"] === "boolean" ? p["supportsUpgrade"] : void 0
1563
+ };
1564
+ }
1565
+ function selectIlpTransport(peer, options = {}) {
1566
+ const needsDuplex = options.needsDuplex ?? false;
1567
+ const http2 = peer.httpEndpoint?.trim() || void 0;
1568
+ const btp = peer.btpEndpoint?.trim() || void 0;
1569
+ const canUpgrade = peer.supportsUpgrade === true;
1570
+ if (needsDuplex) {
1571
+ if (btp) return { kind: "btp", btpEndpoint: btp };
1572
+ if (http2 && canUpgrade) return { kind: "http-upgradable", httpEndpoint: http2 };
1573
+ throw new Error(
1574
+ "Duplex transport required but peer exposes neither a btpEndpoint nor an upgradable httpEndpoint"
1575
+ );
1576
+ }
1577
+ if (http2) return { kind: "http", httpEndpoint: http2, canUpgrade };
1578
+ if (btp) return { kind: "btp", btpEndpoint: btp };
1579
+ throw new Error("Peer exposes neither an httpEndpoint nor a btpEndpoint");
1580
+ }
1581
+
1355
1582
  // src/channel/OnChainChannelClient.ts
1356
1583
  import {
1357
1584
  createPublicClient,
@@ -2548,6 +2775,12 @@ async function initializeHttpMode(config) {
2548
2775
  const effectiveBtpUrl = transport.btpUrl ?? config.btpUrl;
2549
2776
  const effectiveConnectorUrl = transport.connectorUrl ?? config.connectorUrl;
2550
2777
  const settlementInfo = buildSettlementInfo(config);
2778
+ const discoveredPeer = readDiscoveredIlpPeer({
2779
+ btpEndpoint: effectiveBtpUrl,
2780
+ httpEndpoint: config.connectorHttpEndpoint,
2781
+ supportsUpgrade: config.connectorSupportsUpgrade
2782
+ });
2783
+ const transportChoice = discoveredPeer.httpEndpoint || discoveredPeer.btpEndpoint ? selectIlpTransport(discoveredPeer, { needsDuplex: false }) : null;
2551
2784
  let btpClient = null;
2552
2785
  if (effectiveBtpUrl) {
2553
2786
  btpClient = new BtpRuntimeClient({
@@ -2558,7 +2791,20 @@ async function initializeHttpMode(config) {
2558
2791
  });
2559
2792
  await btpClient.connect();
2560
2793
  }
2561
- const runtimeClient = btpClient ?? new HttpRuntimeClient({
2794
+ let httpIlpClient = null;
2795
+ if (transportChoice && (transportChoice.kind === "http" || transportChoice.kind === "http-upgradable")) {
2796
+ httpIlpClient = new HttpIlpClient({
2797
+ httpEndpoint: transportChoice.httpEndpoint,
2798
+ ...config.btpPeerId !== void 0 ? { peerId: config.btpPeerId } : {},
2799
+ ...config.btpAuthToken !== void 0 ? { authToken: config.btpAuthToken } : {},
2800
+ timeout: config.queryTimeout,
2801
+ maxRetries: config.maxRetries,
2802
+ retryDelay: config.retryDelay,
2803
+ ...transport.httpClient !== void 0 ? { httpClient: transport.httpClient } : {},
2804
+ ...transport.createWebSocket !== void 0 ? { createWebSocket: transport.createWebSocket } : {}
2805
+ });
2806
+ }
2807
+ const runtimeClient = httpIlpClient ?? btpClient ?? new HttpRuntimeClient({
2562
2808
  connectorUrl: effectiveConnectorUrl,
2563
2809
  timeout: config.queryTimeout,
2564
2810
  maxRetries: config.maxRetries,
@@ -3304,6 +3550,72 @@ var JsonFileChannelStore = class {
3304
3550
  }
3305
3551
  };
3306
3552
 
3553
+ // src/blob-storage.ts
3554
+ import { buildBlobStorageRequest } from "@toon-protocol/core";
3555
+ var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
3556
+ async function requestBlobStorage(client, secretKey, params) {
3557
+ const bid = params.bid ?? (params.ilpAmount !== void 0 ? String(params.ilpAmount) : void 0);
3558
+ if (bid === void 0 || bid === "") {
3559
+ return {
3560
+ success: false,
3561
+ error: "requestBlobStorage requires a bid (or ilpAmount to derive it)"
3562
+ };
3563
+ }
3564
+ const blobBuffer = Buffer.from(
3565
+ params.blobData.buffer,
3566
+ params.blobData.byteOffset,
3567
+ params.blobData.byteLength
3568
+ );
3569
+ let event;
3570
+ try {
3571
+ event = buildBlobStorageRequest(
3572
+ {
3573
+ blobData: blobBuffer,
3574
+ contentType: params.contentType,
3575
+ bid
3576
+ },
3577
+ secretKey
3578
+ );
3579
+ } catch (error) {
3580
+ return {
3581
+ success: false,
3582
+ error: error instanceof Error ? error.message : String(error)
3583
+ };
3584
+ }
3585
+ const result = await client.publishEvent(event, {
3586
+ destination: params.destination,
3587
+ claim: params.claim,
3588
+ ilpAmount: params.ilpAmount
3589
+ });
3590
+ if (!result.success) {
3591
+ return {
3592
+ success: false,
3593
+ eventId: result.eventId ?? event.id,
3594
+ error: result.error ?? "Blob storage request rejected"
3595
+ };
3596
+ }
3597
+ if (!result.data) {
3598
+ return {
3599
+ success: false,
3600
+ eventId: event.id,
3601
+ error: "FULFILL contained no data; expected base64-encoded Arweave tx ID"
3602
+ };
3603
+ }
3604
+ const txId = decodeUtf8(fromBase64(result.data));
3605
+ if (!ARWEAVE_TX_ID_REGEX.test(txId)) {
3606
+ return {
3607
+ success: false,
3608
+ eventId: event.id,
3609
+ error: `Decoded FULFILL data is not a valid Arweave tx ID: "${txId}"`
3610
+ };
3611
+ }
3612
+ return {
3613
+ success: true,
3614
+ txId,
3615
+ eventId: event.id
3616
+ };
3617
+ }
3618
+
3307
3619
  // src/ToonClient.ts
3308
3620
  var ToonClient = class {
3309
3621
  config;
@@ -3355,6 +3667,28 @@ var ToonClient = class {
3355
3667
  getPublicKey() {
3356
3668
  return getPublicKey2(this.config.secretKey);
3357
3669
  }
3670
+ /**
3671
+ * Sign an unsigned Nostr event template with the client's Nostr secret key,
3672
+ * returning a fully-signed event (id + pubkey + sig).
3673
+ *
3674
+ * This is the key primitive behind the daemon's sign-and-publish path: a UI
3675
+ * or agent supplies only `{ kind, content, tags, created_at }` and never holds
3676
+ * the private key — signing happens here, inside the key owner.
3677
+ */
3678
+ signEvent(template) {
3679
+ return finalizeEvent(template, this.config.secretKey);
3680
+ }
3681
+ /**
3682
+ * Upload bytes to Arweave via the kind:5094 blob-storage DVM (single-packet),
3683
+ * signing the request with this client's Nostr key and paying through its
3684
+ * existing channel. Returns the Arweave tx id on success.
3685
+ *
3686
+ * Backs the daemon's `upload-media` path: the key and claim/channel plumbing
3687
+ * stay inside the client; callers pass only the bytes.
3688
+ */
3689
+ async uploadBlob(params) {
3690
+ return requestBlobStorage(this, this.config.secretKey, params);
3691
+ }
3358
3692
  /**
3359
3693
  * Per-chain settlement readiness for the configured `network` tier, mirroring
3360
3694
  * the townhouse node's status. Returns `undefined` when no named `network` is
@@ -4735,74 +5069,8 @@ function buildPetPurchaseRequest(params) {
4735
5069
  };
4736
5070
  }
4737
5071
 
4738
- // src/blob-storage.ts
4739
- import { buildBlobStorageRequest } from "@toon-protocol/core";
4740
- var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
4741
- async function requestBlobStorage(client, secretKey, params) {
4742
- const bid = params.bid ?? (params.ilpAmount !== void 0 ? String(params.ilpAmount) : void 0);
4743
- if (bid === void 0 || bid === "") {
4744
- return {
4745
- success: false,
4746
- error: "requestBlobStorage requires a bid (or ilpAmount to derive it)"
4747
- };
4748
- }
4749
- const blobBuffer = Buffer.from(
4750
- params.blobData.buffer,
4751
- params.blobData.byteOffset,
4752
- params.blobData.byteLength
4753
- );
4754
- let event;
4755
- try {
4756
- event = buildBlobStorageRequest(
4757
- {
4758
- blobData: blobBuffer,
4759
- contentType: params.contentType,
4760
- bid
4761
- },
4762
- secretKey
4763
- );
4764
- } catch (error) {
4765
- return {
4766
- success: false,
4767
- error: error instanceof Error ? error.message : String(error)
4768
- };
4769
- }
4770
- const result = await client.publishEvent(event, {
4771
- destination: params.destination,
4772
- claim: params.claim,
4773
- ilpAmount: params.ilpAmount
4774
- });
4775
- if (!result.success) {
4776
- return {
4777
- success: false,
4778
- eventId: result.eventId ?? event.id,
4779
- error: result.error ?? "Blob storage request rejected"
4780
- };
4781
- }
4782
- if (!result.data) {
4783
- return {
4784
- success: false,
4785
- eventId: event.id,
4786
- error: "FULFILL contained no data; expected base64-encoded Arweave tx ID"
4787
- };
4788
- }
4789
- const txId = decodeUtf8(fromBase64(result.data));
4790
- if (!ARWEAVE_TX_ID_REGEX.test(txId)) {
4791
- return {
4792
- success: false,
4793
- eventId: event.id,
4794
- error: `Decoded FULFILL data is not a valid Arweave tx ID: "${txId}"`
4795
- };
4796
- }
4797
- return {
4798
- success: true,
4799
- txId,
4800
- eventId: event.id
4801
- };
4802
- }
4803
-
4804
5072
  // src/keys/KeyManager.ts
4805
- import { finalizeEvent } from "nostr-tools/pure";
5073
+ import { finalizeEvent as finalizeEvent2 } from "nostr-tools/pure";
4806
5074
  import { nip19 } from "nostr-tools";
4807
5075
 
4808
5076
  // src/keys/PasskeyAuth.ts
@@ -5588,7 +5856,7 @@ var KeyManager = class {
5588
5856
  this.vault,
5589
5857
  this.identity.nostr.secretKey
5590
5858
  );
5591
- const signedEvent = finalizeEvent(
5859
+ const signedEvent = finalizeEvent2(
5592
5860
  eventTemplate,
5593
5861
  this.identity.nostr.secretKey
5594
5862
  );
@@ -5850,7 +6118,11 @@ export {
5850
6118
  HS_HOSTNAME_MAX_LENGTH,
5851
6119
  HS_HOSTNAME_REGEX,
5852
6120
  HttpConnectorAdmin,
6121
+ HttpIlpClient,
5853
6122
  HttpRuntimeClient,
6123
+ ILP_CLAIM_HEADER,
6124
+ ILP_CLAIM_WRAPPED_HEADER,
6125
+ ILP_PEER_ID_HEADER,
5854
6126
  KeyManager,
5855
6127
  MinaSigner,
5856
6128
  NetworkError,
@@ -5879,6 +6151,7 @@ export {
5879
6151
  generateMnemonic,
5880
6152
  generateRandomIdentity,
5881
6153
  getNetworkStatus,
6154
+ httpEndpointToBtpUrl,
5882
6155
  importKeystore,
5883
6156
  isPrfSupported,
5884
6157
  isRoutableHsHostname,
@@ -5887,9 +6160,11 @@ export {
5887
6160
  parsePetInteractionEvent,
5888
6161
  parsePetInteractionResult,
5889
6162
  parsePetListing,
6163
+ readDiscoveredIlpPeer,
5890
6164
  readMinaDepositTotal,
5891
6165
  requestBlobStorage,
5892
6166
  selectAnonAsset,
6167
+ selectIlpTransport,
5893
6168
  startManagedAnonProxy,
5894
6169
  validateConfig,
5895
6170
  validateMnemonic,