@toon-protocol/client 0.10.0 → 0.12.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
@@ -1,10 +1,3 @@
1
- import {
2
- ANON_ASSETS,
3
- ANON_VERSION,
4
- selectAnonAsset,
5
- startManagedAnonProxy
6
- } from "./chunk-WHAEQLIW.js";
7
-
8
1
  // src/ToonClient.ts
9
2
  import { generateSecretKey as generateSecretKey3, getPublicKey as getPublicKey2, finalizeEvent } from "nostr-tools/pure";
10
3
 
@@ -263,26 +256,49 @@ function getNetworkStatus(config) {
263
256
  if (!network || network === "custom") return void 0;
264
257
  return resolveClientNetwork(network).status;
265
258
  }
259
+ function proxyIlpEndpoint(proxyUrl) {
260
+ if (!proxyUrl) return void 0;
261
+ const trimmed = proxyUrl.replace(/\/+$/, "");
262
+ return /\/ilp$/i.test(trimmed) ? trimmed : `${trimmed}/ilp`;
263
+ }
266
264
  function validateConfig(config) {
267
265
  if (config.connector !== void 0) {
268
266
  throw new ValidationError(
269
267
  "Embedded mode not yet implemented in ToonClient. Use connectorUrl for HTTP mode."
270
268
  );
271
269
  }
272
- if (!config.connectorUrl) {
270
+ if (!config.connectorUrl && !config.proxyUrl) {
273
271
  throw new ValidationError(
274
- 'connectorUrl is required for HTTP mode. Example: "http://localhost:8080"'
272
+ 'connectorUrl (or proxyUrl) is required for HTTP mode. Example: "http://localhost:8080"'
275
273
  );
276
274
  }
277
- try {
278
- const url = new URL(config.connectorUrl);
279
- if (!url.protocol.startsWith("http")) {
280
- throw new Error("Must be HTTP or HTTPS");
275
+ if (config.connectorUrl) {
276
+ try {
277
+ const url = new URL(config.connectorUrl);
278
+ if (!url.protocol.startsWith("http")) {
279
+ throw new Error("Must be HTTP or HTTPS");
280
+ }
281
+ } catch (error) {
282
+ throw new ValidationError(
283
+ `Invalid connectorUrl: must be a valid HTTP/HTTPS URL (e.g., "http://localhost:8080"). Error: ${error instanceof Error ? error.message : String(error)}`
284
+ );
285
+ }
286
+ }
287
+ for (const [field, value] of [
288
+ ["proxyUrl", config.proxyUrl],
289
+ ["faucetUrl", config.faucetUrl]
290
+ ]) {
291
+ if (value === void 0) continue;
292
+ try {
293
+ const url = new URL(value);
294
+ if (!url.protocol.startsWith("http")) {
295
+ throw new Error("Must be HTTP or HTTPS");
296
+ }
297
+ } catch (error) {
298
+ throw new ValidationError(
299
+ `Invalid ${field}: must be a valid HTTP/HTTPS URL. Error: ${error instanceof Error ? error.message : String(error)}`
300
+ );
281
301
  }
282
- } catch (error) {
283
- throw new ValidationError(
284
- `Invalid connectorUrl: must be a valid HTTP/HTTPS URL (e.g., "http://localhost:8080"). Error: ${error instanceof Error ? error.message : String(error)}`
285
- );
286
302
  }
287
303
  if (config.secretKey !== void 0) {
288
304
  if (!config.secretKey || config.secretKey.length !== 32) {
@@ -346,25 +362,6 @@ function validateConfig(config) {
346
362
  );
347
363
  }
348
364
  }
349
- if (config.transport) {
350
- if (config.transport.type === "socks5") {
351
- if (!config.transport.socksProxy?.startsWith("socks5h://")) {
352
- throw new ValidationError(
353
- 'transport.socksProxy must use socks5h:// scheme to prevent DNS leaks. The "h" suffix ensures .anyone hostnames are resolved by the proxy, not locally.'
354
- );
355
- }
356
- } else if (config.transport.type === "gateway") {
357
- if (!config.transport.gatewayUrl) {
358
- throw new ValidationError(
359
- "transport.gatewayUrl is required for gateway transport"
360
- );
361
- }
362
- } else if (config.transport.type !== "direct") {
363
- throw new ValidationError(
364
- `Unknown transport type: "${config.transport.type}"`
365
- );
366
- }
367
- }
368
365
  if (config.chainRpcUrls && config.supportedChains) {
369
366
  for (const chain of Object.keys(config.chainRpcUrls)) {
370
367
  if (!config.supportedChains.includes(chain)) {
@@ -381,19 +378,21 @@ function applyDefaults(rawConfig) {
381
378
  config.mnemonic,
382
379
  config.mnemonicAccountIndex ?? 0
383
380
  ).secretKey : generateSecretKey2());
381
+ const connectorHttpEndpoint = config.connectorHttpEndpoint ?? proxyIlpEndpoint(config.proxyUrl);
382
+ const connectorUrl = config.connectorUrl ?? config.proxyUrl?.replace(/\/+$/, "");
384
383
  let btpUrl = config.btpUrl;
385
- if (!btpUrl && config.connectorUrl) {
384
+ if (!btpUrl && connectorUrl && !connectorHttpEndpoint) {
386
385
  try {
387
- const url = new URL(config.connectorUrl);
386
+ const url = new URL(connectorUrl);
388
387
  const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
389
388
  btpUrl = `${wsProtocol}//${url.hostname}:3000`;
390
389
  } catch {
391
390
  }
392
391
  }
393
392
  let destinationAddress = config.destinationAddress;
394
- if (!destinationAddress && config.connectorUrl) {
393
+ if (!destinationAddress && connectorUrl) {
395
394
  try {
396
- const url = new URL(config.connectorUrl);
395
+ const url = new URL(connectorUrl);
397
396
  if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
398
397
  if (url.port === "8080") {
399
398
  destinationAddress = "g.toon.genesis";
@@ -416,8 +415,10 @@ function applyDefaults(rawConfig) {
416
415
  ...config,
417
416
  secretKey,
418
417
  evmPrivateKey,
419
- connectorUrl: config.connectorUrl,
420
- // Already validated as required
418
+ // Satisfied by connectorUrl OR proxyUrl (validateConfig enforced one of them).
419
+ connectorUrl,
420
+ // Surface the derived `POST /ilp` endpoint so HTTP mode selects HttpIlpClient.
421
+ ...connectorHttpEndpoint ? { connectorHttpEndpoint } : {},
421
422
  relayUrl: config.relayUrl ?? "ws://localhost:7100",
422
423
  queryTimeout: config.queryTimeout ?? 3e4,
423
424
  maxRetries: config.maxRetries ?? 3,
@@ -480,6 +481,15 @@ function isBase64(str) {
480
481
  return /^[A-Za-z0-9+/]*={0,2}$/.test(str);
481
482
  }
482
483
 
484
+ // src/utils/store-envelope.ts
485
+ var REQUEST_LINE = "POST /write HTTP/1.1";
486
+ var HEADERS = ["Host: relay", "Content-Type: application/json"];
487
+ function buildStoreWriteEnvelope(event) {
488
+ const body = JSON.stringify({ event });
489
+ const head = [REQUEST_LINE, ...HEADERS].join("\r\n");
490
+ return encodeUtf8(head + "\r\n\r\n" + body);
491
+ }
492
+
483
493
  // src/modes/http.ts
484
494
  import { BootstrapService, createDiscoveryTracker } from "@toon-protocol/core";
485
495
 
@@ -2698,82 +2708,10 @@ var EvmSigner = class {
2698
2708
  }
2699
2709
  };
2700
2710
 
2701
- // src/transport/index.ts
2702
- function isAnyoneHost(url) {
2703
- if (!url) return false;
2704
- try {
2705
- const withScheme = /:\/\//.test(url) ? url : `ws://${url}`;
2706
- const host = new URL(withScheme).hostname.toLowerCase();
2707
- return host.endsWith(".anyone");
2708
- } catch {
2709
- return false;
2710
- }
2711
- }
2712
- async function resolveTransport(transport, originalBtpUrl, originalConnectorUrl, managedProxyOptions) {
2713
- const hasExplicitProxy = !!transport && (transport.type === "socks5" || transport.type === "gateway");
2714
- const envProxy = process.env["ANYONE_PROXY_URLS"];
2715
- if (!hasExplicitProxy && managedProxyOptions?.managedAnonProxy !== false && !envProxy && isAnyoneHost(originalBtpUrl)) {
2716
- const { startManagedAnonProxy: startManagedAnonProxy2 } = await import("./anon-proxy-W3KMM7GU.js");
2717
- const { createSocks5WebSocketFactory, createSocks5Fetch } = await import("./socks5-WTJBYGME.js");
2718
- const proxy = await startManagedAnonProxy2({
2719
- ...managedProxyOptions?.managedAnonSocksPort !== void 0 ? { socksPort: managedProxyOptions.managedAnonSocksPort } : {}
2720
- });
2721
- try {
2722
- return {
2723
- createWebSocket: createSocks5WebSocketFactory(proxy.socksProxy),
2724
- httpClient: createSocks5Fetch(proxy.socksProxy),
2725
- stopManagedProxy: proxy.stop
2726
- };
2727
- } catch (err) {
2728
- await proxy.stop();
2729
- throw err;
2730
- }
2731
- }
2732
- if (!transport || transport.type === "direct") {
2733
- return {};
2734
- }
2735
- if (transport.type === "socks5") {
2736
- const {
2737
- createSocks5WebSocketFactory,
2738
- createSocks5Fetch,
2739
- probeSocks5Proxy
2740
- } = await import("./socks5-WTJBYGME.js");
2741
- await probeSocks5Proxy(transport.socksProxy);
2742
- return {
2743
- createWebSocket: createSocks5WebSocketFactory(transport.socksProxy),
2744
- httpClient: createSocks5Fetch(transport.socksProxy)
2745
- };
2746
- }
2747
- if (transport.type === "gateway") {
2748
- const { rewriteUrlsForGateway } = await import("./gateway-QOK47RKS.js");
2749
- const rewritten = rewriteUrlsForGateway(
2750
- transport.gatewayUrl,
2751
- originalBtpUrl,
2752
- originalConnectorUrl
2753
- );
2754
- return {
2755
- btpUrl: rewritten.btpUrl,
2756
- connectorUrl: rewritten.connectorUrl
2757
- };
2758
- }
2759
- throw new Error(
2760
- `Unknown transport type: "${transport.type}"`
2761
- );
2762
- }
2763
-
2764
2711
  // src/modes/http.ts
2765
2712
  async function initializeHttpMode(config) {
2766
- const transport = await resolveTransport(
2767
- config.transport,
2768
- config.btpUrl,
2769
- config.connectorUrl,
2770
- {
2771
- ...config.managedAnonProxy !== void 0 ? { managedAnonProxy: config.managedAnonProxy } : {},
2772
- ...config.managedAnonSocksPort !== void 0 ? { managedAnonSocksPort: config.managedAnonSocksPort } : {}
2773
- }
2774
- );
2775
- const effectiveBtpUrl = transport.btpUrl ?? config.btpUrl;
2776
- const effectiveConnectorUrl = transport.connectorUrl ?? config.connectorUrl;
2713
+ const effectiveBtpUrl = config.btpUrl;
2714
+ const effectiveConnectorUrl = config.connectorUrl;
2777
2715
  const settlementInfo = buildSettlementInfo(config);
2778
2716
  const discoveredPeer = readDiscoveredIlpPeer({
2779
2717
  btpEndpoint: effectiveBtpUrl,
@@ -2786,8 +2724,7 @@ async function initializeHttpMode(config) {
2786
2724
  btpClient = new BtpRuntimeClient({
2787
2725
  btpUrl: effectiveBtpUrl,
2788
2726
  peerId: config.btpPeerId ?? `client`,
2789
- authToken: config.btpAuthToken ?? "",
2790
- createWebSocket: transport.createWebSocket
2727
+ authToken: config.btpAuthToken ?? ""
2791
2728
  });
2792
2729
  await btpClient.connect();
2793
2730
  }
@@ -2799,17 +2736,14 @@ async function initializeHttpMode(config) {
2799
2736
  ...config.btpAuthToken !== void 0 ? { authToken: config.btpAuthToken } : {},
2800
2737
  timeout: config.queryTimeout,
2801
2738
  maxRetries: config.maxRetries,
2802
- retryDelay: config.retryDelay,
2803
- ...transport.httpClient !== void 0 ? { httpClient: transport.httpClient } : {},
2804
- ...transport.createWebSocket !== void 0 ? { createWebSocket: transport.createWebSocket } : {}
2739
+ retryDelay: config.retryDelay
2805
2740
  });
2806
2741
  }
2807
2742
  const runtimeClient = httpIlpClient ?? btpClient ?? new HttpRuntimeClient({
2808
2743
  connectorUrl: effectiveConnectorUrl,
2809
2744
  timeout: config.queryTimeout,
2810
2745
  maxRetries: config.maxRetries,
2811
- retryDelay: config.retryDelay,
2812
- httpClient: transport.httpClient
2746
+ retryDelay: config.retryDelay
2813
2747
  });
2814
2748
  let onChainChannelClient = null;
2815
2749
  if (config.chainRpcUrls) {
@@ -2854,10 +2788,7 @@ async function initializeHttpMode(config) {
2854
2788
  runtimeClient,
2855
2789
  adminClient: null,
2856
2790
  btpClient,
2857
- onChainChannelClient,
2858
- // Teardown handle for a managed `anon` proxy this init STARTED (undefined
2859
- // for explicit-proxy/direct/gateway). ToonClient.stop() invokes it.
2860
- stopManagedProxy: transport.stopManagedProxy
2791
+ onChainChannelClient
2861
2792
  };
2862
2793
  }
2863
2794
 
@@ -3616,6 +3547,258 @@ async function requestBlobStorage(client, secretKey, params) {
3616
3547
  };
3617
3548
  }
3618
3549
 
3550
+ // src/adapters/Http402Client.ts
3551
+ var Http402Client = class {
3552
+ fetchImpl;
3553
+ resolveClaim;
3554
+ createIlpClient;
3555
+ needsDuplex;
3556
+ constructor(config = {}) {
3557
+ this.fetchImpl = config.fetch ?? fetch;
3558
+ this.resolveClaim = config.resolveClaim;
3559
+ this.createIlpClient = config.createIlpClient ?? ((httpEndpoint) => new HttpIlpClient({ httpEndpoint }));
3560
+ this.needsDuplex = config.needsDuplex ?? false;
3561
+ }
3562
+ /**
3563
+ * `fetch()`-like entry point. Issues the request; on `402` parses the x402
3564
+ * challenge and — when a usable `toon-channel` offer is present and a claim
3565
+ * resolver is configured — pays over TOON and returns the reconstructed
3566
+ * `Response`. Otherwise returns the original 402 unchanged (AC5).
3567
+ */
3568
+ async fetch(url, opts = {}) {
3569
+ const method = (opts.method ?? "GET").toUpperCase();
3570
+ const probe = await this.fetchImpl(url, {
3571
+ method,
3572
+ ...opts.headers ? { headers: opts.headers } : {},
3573
+ ...opts.body !== void 0 ? { body: opts.body } : {},
3574
+ ...opts.timeout !== void 0 ? { signal: AbortSignal.timeout(opts.timeout) } : {}
3575
+ });
3576
+ if (probe.status !== 402) return probe;
3577
+ const challenge = await parseX402Challenge(probe.clone());
3578
+ const accept = challenge.toonChannel;
3579
+ if (!accept || !this.resolveClaim) return probe;
3580
+ return this.payOverToon(url, method, opts, accept, this.resolveClaim);
3581
+ }
3582
+ /**
3583
+ * Open/reuse a channel (via the injected claim resolver), serialize the HTTP
3584
+ * request into the ILP packet `data`, send it to `POST /ilp` with the claim,
3585
+ * and reconstruct the origin `Response` from the FULFILL `data`.
3586
+ */
3587
+ async payOverToon(url, method, opts, accept, resolveClaim) {
3588
+ const destination = opts.destination ?? accept.destination;
3589
+ const claim = await resolveClaim(destination, accept.amount);
3590
+ const requestBytes = serializeHttpRequest({
3591
+ method,
3592
+ url,
3593
+ headers: opts.headers,
3594
+ body: opts.body
3595
+ });
3596
+ const peer = {
3597
+ httpEndpoint: accept.httpEndpoint,
3598
+ supportsUpgrade: accept.supportsUpgrade
3599
+ };
3600
+ const choice = selectIlpTransport(peer, {
3601
+ needsDuplex: this.needsDuplex
3602
+ });
3603
+ const ilpClient = this.createIlpClient(accept.httpEndpoint);
3604
+ const result = await this.sendOverChoice(
3605
+ ilpClient,
3606
+ choice,
3607
+ {
3608
+ destination,
3609
+ amount: String(accept.amount),
3610
+ data: toBase64(requestBytes),
3611
+ ...opts.timeout !== void 0 ? { timeout: opts.timeout } : {}
3612
+ },
3613
+ claim
3614
+ );
3615
+ if (!result.accepted) {
3616
+ throw new ConnectorError(
3617
+ `h402 payment rejected by connector: ${result.code ?? "F00"} ${result.message ?? ""}`.trim()
3618
+ );
3619
+ }
3620
+ if (!result.data) {
3621
+ throw new ConnectorError(
3622
+ "h402 FULFILL carried no data (expected an HTTP response payload)"
3623
+ );
3624
+ }
3625
+ return parseHttpResponse(fromBase64(result.data));
3626
+ }
3627
+ /**
3628
+ * Send the serialized HTTP-in-ILP PREPARE over the selected transport.
3629
+ *
3630
+ * - `http` / `http-upgradable`: stateless one-shot `POST /ilp` with the claim.
3631
+ * - `http-upgradable` additionally exercises {@link HttpIlpClient.upgradeToBtp}
3632
+ * for the duplex/streaming path (AC4). v1 still drives the actual write over
3633
+ * the one-shot HTTP method even after upgrading — full duplex body streaming
3634
+ * is a documented follow-up — but the upgrade call path is wired here.
3635
+ * - `btp`: not reachable from h402 (the x402 offer only carries an
3636
+ * `httpEndpoint`); guarded for completeness.
3637
+ */
3638
+ async sendOverChoice(ilpClient, choice, params, claim) {
3639
+ if (choice.kind === "http-upgradable") {
3640
+ const btp = await ilpClient.upgradeToBtp();
3641
+ try {
3642
+ return await btp.sendIlpPacketWithClaim(
3643
+ params,
3644
+ claim
3645
+ );
3646
+ } finally {
3647
+ await btp.disconnect().catch(() => {
3648
+ });
3649
+ }
3650
+ }
3651
+ if (choice.kind === "btp") {
3652
+ throw new ToonClientError(
3653
+ "h402 offer resolved to a BTP-only transport; the x402 toon-channel entry must advertise an httpEndpoint",
3654
+ "INVALID_STATE"
3655
+ );
3656
+ }
3657
+ return ilpClient.sendIlpPacketWithClaim(params, claim);
3658
+ }
3659
+ };
3660
+ function readString(obj, keys) {
3661
+ for (const k of keys) {
3662
+ const v = obj[k];
3663
+ if (typeof v === "string" && v.trim().length > 0) return v.trim();
3664
+ }
3665
+ return void 0;
3666
+ }
3667
+ function readAmount(obj, keys) {
3668
+ for (const k of keys) {
3669
+ const v = obj[k];
3670
+ if (typeof v === "bigint") return v;
3671
+ if (typeof v === "number" && Number.isFinite(v)) return BigInt(Math.trunc(v));
3672
+ if (typeof v === "string" && /^\d+$/.test(v.trim())) return BigInt(v.trim());
3673
+ }
3674
+ return void 0;
3675
+ }
3676
+ async function parseX402Challenge(response) {
3677
+ let body;
3678
+ try {
3679
+ body = await response.json();
3680
+ } catch {
3681
+ return {};
3682
+ }
3683
+ return parseX402Body(body);
3684
+ }
3685
+ function parseX402Body(body) {
3686
+ if (typeof body !== "object" || body === null) return {};
3687
+ const b = body;
3688
+ const version = typeof b["x402Version"] === "number" ? b["x402Version"] : void 0;
3689
+ const accepts = Array.isArray(b["accepts"]) ? b["accepts"] : [];
3690
+ for (const raw of accepts) {
3691
+ if (typeof raw !== "object" || raw === null) continue;
3692
+ const entry = raw;
3693
+ const scheme = readString(entry, ["scheme"]);
3694
+ if (scheme !== "toon-channel") continue;
3695
+ const destination = readString(entry, [
3696
+ "destination",
3697
+ "ilpAddress",
3698
+ "payTo"
3699
+ ]);
3700
+ const httpEndpoint = readString(entry, [
3701
+ "httpEndpoint",
3702
+ "ilpEndpoint",
3703
+ "endpoint"
3704
+ ]);
3705
+ const amount = readAmount(entry, ["amount", "price", "maxAmountRequired"]);
3706
+ if (!destination || !httpEndpoint || amount === void 0) continue;
3707
+ const network = readString(entry, ["network", "chain"]);
3708
+ const supportsUpgrade = entry["supportsUpgrade"] === true || entry["upgradable"] === true;
3709
+ return {
3710
+ ...version !== void 0 ? { x402Version: version } : {},
3711
+ toonChannel: {
3712
+ scheme: "toon-channel",
3713
+ ...network !== void 0 ? { network } : {},
3714
+ destination,
3715
+ amount,
3716
+ httpEndpoint,
3717
+ supportsUpgrade
3718
+ }
3719
+ };
3720
+ }
3721
+ return version !== void 0 ? { x402Version: version } : {};
3722
+ }
3723
+ var CRLF = "\r\n";
3724
+ function concatHeadAndBody(head, body) {
3725
+ const headBytes = encodeUtf8(head);
3726
+ const out = new Uint8Array(headBytes.length + body.length);
3727
+ out.set(headBytes, 0);
3728
+ out.set(body, headBytes.length);
3729
+ return out;
3730
+ }
3731
+ function bodyToBytes(body) {
3732
+ if (body === void 0) return new Uint8Array(0);
3733
+ return typeof body === "string" ? encodeUtf8(body) : body;
3734
+ }
3735
+ function serializeHttpRequest(req) {
3736
+ const u = new URL(req.url);
3737
+ const target = `${u.pathname}${u.search}` || "/";
3738
+ const bodyBytes = bodyToBytes(req.body);
3739
+ const headers = /* @__PURE__ */ new Map();
3740
+ const put = (name, value) => headers.set(name.toLowerCase(), `${name}: ${value}`);
3741
+ const has = (name) => headers.has(name.toLowerCase());
3742
+ for (const [name, value] of Object.entries(req.headers ?? {})) {
3743
+ put(name, value);
3744
+ }
3745
+ if (!has("host")) put("Host", u.host);
3746
+ if (bodyBytes.length > 0 && !has("content-length")) {
3747
+ put("Content-Length", String(bodyBytes.length));
3748
+ }
3749
+ const lines = [
3750
+ `${req.method.toUpperCase()} ${target} HTTP/1.1`,
3751
+ ...headers.values()
3752
+ ];
3753
+ const head = lines.join(CRLF) + CRLF + CRLF;
3754
+ return concatHeadAndBody(head, bodyBytes);
3755
+ }
3756
+ function findHeaderEnd(bytes) {
3757
+ for (let i = 0; i + 3 < bytes.length; i++) {
3758
+ if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) {
3759
+ return i + 4;
3760
+ }
3761
+ }
3762
+ return -1;
3763
+ }
3764
+ function parseHttpResponse(bytes) {
3765
+ const headerEnd = findHeaderEnd(bytes);
3766
+ const headBytes = headerEnd === -1 ? bytes : bytes.subarray(0, headerEnd - 2);
3767
+ const body = headerEnd === -1 ? new Uint8Array(0) : bytes.subarray(headerEnd);
3768
+ const headText = decodeUtf8(headBytes);
3769
+ const lines = headText.split(CRLF).filter((l) => l.length > 0);
3770
+ const statusLine = lines.shift();
3771
+ if (!statusLine) {
3772
+ throw new ConnectorError(
3773
+ "h402 response payload had no HTTP status line"
3774
+ );
3775
+ }
3776
+ const match = /^HTTP\/\d\.\d\s+(\d{3})(?:\s+(.*))?$/.exec(statusLine.trim());
3777
+ if (!match) {
3778
+ throw new ConnectorError(
3779
+ `h402 response payload had a malformed status line: "${statusLine}"`
3780
+ );
3781
+ }
3782
+ const status = parseInt(match[1], 10);
3783
+ const statusText = match[2] ?? "";
3784
+ const headers = new Headers();
3785
+ for (const line of lines) {
3786
+ const idx = line.indexOf(":");
3787
+ if (idx === -1) continue;
3788
+ const name = line.slice(0, idx).trim();
3789
+ const value = line.slice(idx + 1).trim();
3790
+ if (name.length === 0) continue;
3791
+ headers.append(name, value);
3792
+ }
3793
+ const nullBodyStatus = status === 101 || status === 204 || status === 205 || status === 304;
3794
+ const init = { status, headers };
3795
+ if (statusText) init.statusText = statusText;
3796
+ return new Response(
3797
+ nullBodyStatus || body.length === 0 ? null : body.slice(),
3798
+ init
3799
+ );
3800
+ }
3801
+
3619
3802
  // src/ToonClient.ts
3620
3803
  var ToonClient = class {
3621
3804
  config;
@@ -3771,13 +3954,7 @@ var ToonClient = class {
3771
3954
  }
3772
3955
  }
3773
3956
  const initialization = await initializeHttpMode(this.config);
3774
- const {
3775
- bootstrapService,
3776
- discoveryTracker,
3777
- runtimeClient,
3778
- btpClient,
3779
- stopManagedProxy
3780
- } = initialization;
3957
+ const { bootstrapService, discoveryTracker, runtimeClient, btpClient } = initialization;
3781
3958
  if (this.channelManager) {
3782
3959
  const cm = this.channelManager;
3783
3960
  const nostrPubkey = this.getPublicKey();
@@ -3865,8 +4042,7 @@ var ToonClient = class {
3865
4042
  discoveryTracker,
3866
4043
  runtimeClient,
3867
4044
  peersDiscovered: bootstrapResults.length,
3868
- btpClient: btpClient ?? void 0,
3869
- ...stopManagedProxy ? { stopManagedProxy } : {}
4045
+ btpClient: btpClient ?? void 0
3870
4046
  };
3871
4047
  return {
3872
4048
  peersDiscovered: bootstrapResults.length,
@@ -3902,13 +4078,9 @@ var ToonClient = class {
3902
4078
  const toonData = this.config.toonEncoder(event);
3903
4079
  const basePricePerByte = 10n;
3904
4080
  const amount = options?.ilpAmount !== void 0 ? String(options.ilpAmount) : String(BigInt(toonData.length) * basePricePerByte);
4081
+ const writeData = buildStoreWriteEnvelope(event);
3905
4082
  const destination = options?.destination ?? this.config.destinationAddress;
3906
- if (!this.state.btpClient) {
3907
- throw new ToonClientError(
3908
- "BTP client required for publishing. Configure btpUrl.",
3909
- "NO_BTP_CLIENT"
3910
- );
3911
- }
4083
+ const transport = this.getClaimTransport();
3912
4084
  let claimMessage;
3913
4085
  if (options?.claim) {
3914
4086
  claimMessage = this.buildClaimMessageForProof(options.claim);
@@ -3937,13 +4109,11 @@ var ToonClient = class {
3937
4109
  "MISSING_CLAIM"
3938
4110
  );
3939
4111
  }
3940
- const response = await this.state.btpClient.sendIlpPacketWithClaim(
4112
+ const response = await transport.sendIlpPacketWithClaim(
3941
4113
  {
3942
4114
  destination,
3943
4115
  amount,
3944
- data: toBase64(
3945
- toonData instanceof Uint8Array ? toonData : new Uint8Array(toonData)
3946
- )
4116
+ data: toBase64(writeData)
3947
4117
  },
3948
4118
  claimMessage
3949
4119
  );
@@ -3971,6 +4141,46 @@ var ToonClient = class {
3971
4141
  );
3972
4142
  }
3973
4143
  }
4144
+ /**
4145
+ * Payment-aware HTTP fetch over TOON (issue #50). A `fetch()`-like method that
4146
+ * makes paying for an HTTP resource transparent:
4147
+ *
4148
+ * 1. Issues the HTTP request to `url`.
4149
+ * 2. On `402`, parses the x402 `accepts` array and selects the
4150
+ * `toon-channel` entry (see {@link Http402Client} for the wire shape).
4151
+ * 3. Opens/reuses a payment channel for the entry's ILP destination (via
4152
+ * ChannelManager), signs a balance proof for the demanded price, and
4153
+ * re-sends the SAME HTTP request as a transparent HTTP-in-ILP packet to
4154
+ * the connector's `POST /ilp` (via {@link HttpIlpClient}), with the claim
4155
+ * in the `ILP-Payment-Channel-Claim` header.
4156
+ * 4. Reconstructs and returns a standard Web `Response` from the FULFILL
4157
+ * `data`. The caller never sees ILP.
4158
+ *
4159
+ * If the origin offers no `toon-channel` entry, the original `402` Response is
4160
+ * returned unchanged (the caller sees the vanilla x402 challenge).
4161
+ *
4162
+ * The channel/claim plumbing is wired to the live ChannelManager + per-chain
4163
+ * signer via `resolveClaimForDestination` — identical to `publishEvent`. The
4164
+ * `amount` paid comes from the selected x402 entry (the resource's price).
4165
+ *
4166
+ * @throws {ToonClientError} If the client is not started.
4167
+ * @throws {ConnectorError} If the connector rejects the payment or returns no
4168
+ * HTTP payload.
4169
+ */
4170
+ async h402Fetch(url, opts) {
4171
+ if (!this.state) {
4172
+ throw new ToonClientError(
4173
+ "Client not started. Call start() first.",
4174
+ "INVALID_STATE"
4175
+ );
4176
+ }
4177
+ const client = new Http402Client({
4178
+ ...this.channelManager ? {
4179
+ resolveClaim: (destination, amount) => this.resolveClaimForDestination(destination, amount)
4180
+ } : {}
4181
+ });
4182
+ return client.fetch(url, opts);
4183
+ }
3974
4184
  /**
3975
4185
  * Sends a raw swap ILP packet (Story 12.5) to a Mill peer with an attached
3976
4186
  * balance-proof claim. This is a lower-level surface than `publishEvent`:
@@ -3983,7 +4193,7 @@ var ToonClient = class {
3983
4193
  * matching `destination`,
3984
4194
  * (c) neither -> throw MISSING_CLAIM.
3985
4195
  *
3986
- * @throws {ToonClientError} INVALID_STATE / NO_BTP_CLIENT / MISSING_CLAIM
4196
+ * @throws {ToonClientError} INVALID_STATE / NO_ILP_TRANSPORT / MISSING_CLAIM
3987
4197
  */
3988
4198
  async sendSwapPacket(params) {
3989
4199
  if (!this.state) {
@@ -3992,18 +4202,13 @@ var ToonClient = class {
3992
4202
  "INVALID_STATE"
3993
4203
  );
3994
4204
  }
3995
- if (!this.state.btpClient) {
3996
- throw new ToonClientError(
3997
- "BTP client required for sending swap packets. Configure btpUrl.",
3998
- "NO_BTP_CLIENT"
3999
- );
4000
- }
4205
+ const transport = this.getClaimTransport();
4001
4206
  const claimMessage = await this.resolveClaimForDestination(
4002
4207
  params.destination,
4003
4208
  params.amount,
4004
4209
  params.claim
4005
4210
  );
4006
- return this.state.btpClient.sendIlpPacketWithClaim(
4211
+ return transport.sendIlpPacketWithClaim(
4007
4212
  {
4008
4213
  destination: params.destination,
4009
4214
  amount: String(params.amount),
@@ -4043,6 +4248,51 @@ var ToonClient = class {
4043
4248
  }
4044
4249
  return EvmSigner.buildClaimMessage(claim, this.getPublicKey());
4045
4250
  }
4251
+ /**
4252
+ * Resolve the ILP transport for a paid (claim-bearing) write.
4253
+ *
4254
+ * The connector is a payment-proxy: paid writes carry an ILP PREPARE plus the
4255
+ * signed payment-channel claim. Either transport speaks the SAME claim
4256
+ * contract — the BTP `payment-channel-claim` protocolData entry and the
4257
+ * ILP-over-HTTP `ILP-Payment-Channel-Claim` header serialize the same claim
4258
+ * JSON — so we route through whichever transport is ACTIVE rather than
4259
+ * hard-requiring BTP.
4260
+ *
4261
+ * Selection (mirrors `modes/http.ts` runtime-client precedence):
4262
+ * 1. `runtimeClient` when it implements `sendIlpPacketWithClaim` — this is
4263
+ * the HttpIlpClient (proxy `POST /ilp`) when a `proxyUrl`/
4264
+ * `connectorHttpEndpoint` is configured, else the BtpRuntimeClient.
4265
+ * 2. `btpClient` as an explicit fallback (always present when `btpUrl` is set).
4266
+ *
4267
+ * The level-3 `HttpRuntimeClient` (connector-admin HTTP, no `btpUrl` AND no
4268
+ * proxy) does NOT implement `sendIlpPacketWithClaim`; in that case there is no
4269
+ * paid-write transport and we throw a clear, actionable error.
4270
+ *
4271
+ * @throws {ToonClientError} NO_ILP_TRANSPORT when no active transport can send
4272
+ * a packet+claim.
4273
+ */
4274
+ getClaimTransport() {
4275
+ const state = this.state;
4276
+ if (!state) {
4277
+ throw new ToonClientError(
4278
+ "Client not started. Call start() first.",
4279
+ "INVALID_STATE"
4280
+ );
4281
+ }
4282
+ const candidates = [
4283
+ state.runtimeClient,
4284
+ state.btpClient
4285
+ ];
4286
+ for (const candidate of candidates) {
4287
+ if (candidate && typeof candidate.sendIlpPacketWithClaim === "function") {
4288
+ return candidate;
4289
+ }
4290
+ }
4291
+ throw new ToonClientError(
4292
+ "No ILP transport for paid writes. Configure `proxyUrl`/`connectorHttpEndpoint` (route through the connector proxy over ILP-over-HTTP) or `btpUrl` (BTP socket).",
4293
+ "NO_ILP_TRANSPORT"
4294
+ );
4295
+ }
4046
4296
  /**
4047
4297
  * Shared claim-resolution logic used by `publishEvent` and `sendSwapPacket`.
4048
4298
  * TODO(12.5 followup): also factor `publishEvent`'s inline claim resolution
@@ -4231,14 +4481,9 @@ var ToonClient = class {
4231
4481
  "MISSING_CLAIM"
4232
4482
  );
4233
4483
  }
4234
- if (!this.state.btpClient) {
4235
- throw new ToonClientError(
4236
- "BTP client required for sending payments. Configure btpUrl.",
4237
- "NO_BTP_CLIENT"
4238
- );
4239
- }
4484
+ const transport = this.getClaimTransport();
4240
4485
  const claimMessage = this.buildClaimMessageForProof(params.claim);
4241
- return this.state.btpClient.sendIlpPacketWithClaim(
4486
+ return transport.sendIlpPacketWithClaim(
4242
4487
  ilpParams,
4243
4488
  claimMessage
4244
4489
  );
@@ -4256,14 +4501,10 @@ var ToonClient = class {
4256
4501
  if (!this.state) {
4257
4502
  throw new ToonClientError("Client not started", "INVALID_STATE");
4258
4503
  }
4259
- const stopManagedProxy = this.state.stopManagedProxy;
4260
4504
  try {
4261
4505
  if (this.state.btpClient) {
4262
4506
  await this.state.btpClient.disconnect();
4263
4507
  }
4264
- if (stopManagedProxy) {
4265
- await stopManagedProxy();
4266
- }
4267
4508
  this.state = null;
4268
4509
  } catch (error) {
4269
4510
  throw new ToonClientError(
@@ -4311,26 +4552,6 @@ var ToonClient = class {
4311
4552
  }
4312
4553
  };
4313
4554
 
4314
- // src/transport/hs-hostname.ts
4315
- var HS_HOSTNAME_REGEX = /^[a-z2-7]+\.anyone$/;
4316
- var HS_HOSTNAME_MAX_LENGTH = 80;
4317
- function isRoutableHsHostname(s) {
4318
- return typeof s === "string" && s.length <= HS_HOSTNAME_MAX_LENGTH && HS_HOSTNAME_REGEX.test(s);
4319
- }
4320
- function assertRoutableHsHostname(hostname) {
4321
- if (typeof hostname === "string" && /\.anon$/.test(hostname)) {
4322
- throw new Error(
4323
- `"${hostname}" is not a routable hidden-service address; use the .anyone TLD (e.g. "${hostname.replace(/\.anon$/, ".anyone")}"). The anon daemon only resolves hidden services under .anyone \u2014 a .anon name is treated as a clearnet address and fails (HostUnreachable).`
4324
- );
4325
- }
4326
- if (!isRoutableHsHostname(hostname)) {
4327
- throw new Error(
4328
- `Invalid hidden-service hostname: ${JSON.stringify(hostname)}. Expected a base32 .anyone address matching ${HS_HOSTNAME_REGEX}.`
4329
- );
4330
- }
4331
- return hostname;
4332
- }
4333
-
4334
4555
  // src/adapters/HttpConnectorAdmin.ts
4335
4556
  var HttpConnectorAdmin = class {
4336
4557
  adminUrl;
@@ -5069,6 +5290,75 @@ function buildPetPurchaseRequest(params) {
5069
5290
  };
5070
5291
  }
5071
5292
 
5293
+ // src/faucet.ts
5294
+ function faucetPath(chain) {
5295
+ switch (chain) {
5296
+ case "evm":
5297
+ return "/api/request";
5298
+ case "solana":
5299
+ return "/api/solana/request";
5300
+ case "mina":
5301
+ return "/api/mina/request";
5302
+ }
5303
+ }
5304
+ async function fundWallet(faucetUrl, address, chain, options = {}) {
5305
+ if (!faucetUrl) {
5306
+ throw new Error("fundWallet: faucetUrl is required");
5307
+ }
5308
+ if (!address) {
5309
+ throw new Error("fundWallet: address is required");
5310
+ }
5311
+ if (chain === "solana" || chain === "mina") {
5312
+ throw new Error(
5313
+ `fundWallet: ${chain} faucet funding is deferred (WS3) \u2014 not yet implemented`
5314
+ );
5315
+ }
5316
+ const base = faucetUrl.replace(/\/+$/, "");
5317
+ const url = `${base}${faucetPath(chain)}`;
5318
+ const fetchImpl = options.fetchImpl ?? fetch;
5319
+ const timeout = options.timeout ?? 3e4;
5320
+ const controller = new AbortController();
5321
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
5322
+ let response;
5323
+ try {
5324
+ response = await fetchImpl(url, {
5325
+ method: "POST",
5326
+ headers: { "Content-Type": "application/json" },
5327
+ body: JSON.stringify({ address }),
5328
+ signal: controller.signal
5329
+ });
5330
+ } catch (error) {
5331
+ if (error instanceof Error && error.name === "AbortError") {
5332
+ throw new NetworkError(
5333
+ `Faucet request timed out after ${timeout}ms (${url})`,
5334
+ error
5335
+ );
5336
+ }
5337
+ throw new NetworkError(
5338
+ `Faucet request failed (${url}): ${error instanceof Error ? error.message : String(error)}`,
5339
+ error instanceof Error ? error : void 0
5340
+ );
5341
+ } finally {
5342
+ clearTimeout(timeoutId);
5343
+ }
5344
+ if (!response.ok) {
5345
+ const detail = await response.text().catch(() => "");
5346
+ throw new NetworkError(
5347
+ `Faucet responded ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""} (${url})`
5348
+ );
5349
+ }
5350
+ const body = await response.text().catch(() => "");
5351
+ let parsed = body;
5352
+ if (body) {
5353
+ try {
5354
+ parsed = JSON.parse(body);
5355
+ } catch {
5356
+ parsed = body;
5357
+ }
5358
+ }
5359
+ return { chain, address, response: parsed };
5360
+ }
5361
+
5072
5362
  // src/keys/KeyManager.ts
5073
5363
  import { finalizeEvent as finalizeEvent2 } from "nostr-tools/pure";
5074
5364
  import { nip19 } from "nostr-tools";
@@ -6109,14 +6399,11 @@ function writeKeystoreFile(path, keystore) {
6109
6399
  });
6110
6400
  }
6111
6401
  export {
6112
- ANON_ASSETS,
6113
- ANON_VERSION,
6114
6402
  BtpRuntimeClient,
6115
6403
  ChannelManager,
6116
6404
  ConnectorError,
6117
6405
  EvmSigner,
6118
- HS_HOSTNAME_MAX_LENGTH,
6119
- HS_HOSTNAME_REGEX,
6406
+ Http402Client,
6120
6407
  HttpConnectorAdmin,
6121
6408
  HttpIlpClient,
6122
6409
  HttpRuntimeClient,
@@ -6133,13 +6420,13 @@ export {
6133
6420
  ValidationError,
6134
6421
  applyDefaults,
6135
6422
  applyNetworkPresets,
6136
- assertRoutableHsHostname,
6137
6423
  buildBackupEvent,
6138
6424
  buildBackupFilter,
6139
6425
  buildPetInteractionRequest,
6140
6426
  buildPetListingEvent,
6141
6427
  buildPetPurchaseRequest,
6142
6428
  buildSettlementInfo,
6429
+ buildStoreWriteEnvelope,
6143
6430
  decryptMnemonic2 as decryptMnemonic,
6144
6431
  deriveFromNsec,
6145
6432
  deriveFullIdentity,
@@ -6147,6 +6434,7 @@ export {
6147
6434
  encryptMnemonic2 as encryptMnemonic,
6148
6435
  filterPetDvmProviders,
6149
6436
  filterPetListings,
6437
+ fundWallet,
6150
6438
  generateKeystore,
6151
6439
  generateMnemonic,
6152
6440
  generateRandomIdentity,
@@ -6154,18 +6442,20 @@ export {
6154
6442
  httpEndpointToBtpUrl,
6155
6443
  importKeystore,
6156
6444
  isPrfSupported,
6157
- isRoutableHsHostname,
6158
6445
  loadKeystore,
6159
6446
  parseBackupPayload,
6447
+ parseHttpResponse,
6160
6448
  parsePetInteractionEvent,
6161
6449
  parsePetInteractionResult,
6162
6450
  parsePetListing,
6451
+ parseX402Body,
6452
+ parseX402Challenge,
6453
+ proxyIlpEndpoint,
6163
6454
  readDiscoveredIlpPeer,
6164
6455
  readMinaDepositTotal,
6165
6456
  requestBlobStorage,
6166
- selectAnonAsset,
6167
6457
  selectIlpTransport,
6168
- startManagedAnonProxy,
6458
+ serializeHttpRequest,
6169
6459
  validateConfig,
6170
6460
  validateMnemonic,
6171
6461
  withRetry,