@toon-protocol/client-mcp 0.1.0 → 0.3.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.
Files changed (36) hide show
  1. package/README.md +19 -38
  2. package/dist/app/index.html +166 -0
  3. package/dist/{chunk-5KFXUT5Q.js → chunk-CQ2QIZ6Z.js} +864 -604
  4. package/dist/chunk-CQ2QIZ6Z.js.map +1 -0
  5. package/dist/{chunk-FSS45ZX3.js → chunk-DLYE6U2Z.js} +1516 -316
  6. package/dist/chunk-DLYE6U2Z.js.map +1 -0
  7. package/dist/{chunk-3NAWISI5.js → chunk-QFHCXJ2V.js} +348 -101
  8. package/dist/chunk-QFHCXJ2V.js.map +1 -0
  9. package/dist/chunk-XV52IHVR.js +977 -0
  10. package/dist/chunk-XV52IHVR.js.map +1 -0
  11. package/dist/daemon.js +3 -4
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/e2e/run-journey.d.ts +1 -0
  14. package/dist/e2e/run-journey.js +19953 -0
  15. package/dist/e2e/run-journey.js.map +1 -0
  16. package/dist/{ed25519-2QVPINLS.js → ed25519-2LFQXLYS.js} +6 -2
  17. package/dist/index.d.ts +282 -68
  18. package/dist/index.js +380 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp.js +39 -6
  21. package/dist/mcp.js.map +1 -1
  22. package/package.json +11 -7
  23. package/dist/anon-proxy-W3KMM7GU-FN7ZJY7P.js +0 -25
  24. package/dist/chunk-3NAWISI5.js.map +0 -1
  25. package/dist/chunk-5KFXUT5Q.js.map +0 -1
  26. package/dist/chunk-FSS45ZX3.js.map +0 -1
  27. package/dist/chunk-SKQTKZIH.js +0 -278
  28. package/dist/chunk-SKQTKZIH.js.map +0 -1
  29. package/dist/chunk-ZQKYZJWT.js +0 -359
  30. package/dist/chunk-ZQKYZJWT.js.map +0 -1
  31. package/dist/ed25519-2QVPINLS.js.map +0 -1
  32. package/dist/gateway-QOK47RKS-SEGTXBR3.js +0 -16
  33. package/dist/gateway-QOK47RKS-SEGTXBR3.js.map +0 -1
  34. package/dist/socks5-WTJBYGME-6COK4LXW.js +0 -139
  35. package/dist/socks5-WTJBYGME-6COK4LXW.js.map +0 -1
  36. /package/dist/{anon-proxy-W3KMM7GU-FN7ZJY7P.js.map → ed25519-2LFQXLYS.js.map} +0 -0
@@ -13,10 +13,7 @@ import {
13
13
  isEventExpired,
14
14
  parseIlpPeerInfo,
15
15
  readConfigFile
16
- } from "./chunk-5KFXUT5Q.js";
17
- import {
18
- startManagedAnonProxy
19
- } from "./chunk-SKQTKZIH.js";
16
+ } from "./chunk-CQ2QIZ6Z.js";
20
17
  import {
21
18
  __require
22
19
  } from "./chunk-F22GNSF6.js";
@@ -33,10 +30,12 @@ function hasConfiguredIdentity(file) {
33
30
  );
34
31
  }
35
32
  var CONFIG_HELP = {
36
- transport: "Point btpUrl/relayUrl at your apex. For an .anyone hidden service use ws://<host>.anyone:3000/btp + ws://<host>.anyone:7100 \u2014 the daemon auto-routes through a managed anon proxy. For a direct apex use ws://<host>:3000/btp + ws://<host>:7100.",
37
- btpUrl: "REQUIRED. BTP WebSocket URL of the apex/connector for paid writes.",
33
+ transport: "Configure ONE uplink for paid writes: either `proxyUrl` (connector payment-proxy over ILP-over-HTTP, e.g. https://proxy.devnet.toonprotocol.dev) OR `btpUrl` (BTP WebSocket, e.g. ws://<host>:3000/btp). Reads are always free over relayUrl.",
34
+ proxyUrl: "Connector-proxy base URL (deployed devnet/testnet edge). When set, paid writes route through `POST /ilp` and no BTP socket is needed. Set `destination` to the apex ILP address (e.g. g.proxy for devnet).",
35
+ faucetUrl: "Devnet faucet base URL (e.g. https://faucet.devnet.toonprotocol.dev) used to drip test funds before publishing.",
36
+ btpUrl: "BTP WebSocket URL of the apex/connector for paid writes. Optional when `proxyUrl` is set.",
38
37
  relayUrl: "Town relay WebSocket URL for FREE reads. Default ws://localhost:7100.",
39
- managedAnonProxy: "Optional override \u2014 leave unset to auto-infer from btpUrl (.anyone host \u2192 managed anon proxy, anything else \u2192 direct).",
38
+ destination: "Default ILP publish destination (apex address). Default g.townhouse.town; set to g.proxy for the deployed devnet proxy.",
40
39
  keystorePath: "Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase."
41
40
  };
42
41
  async function scaffoldFirstRun(opts = {}) {
@@ -75,13 +74,14 @@ async function scaffoldFirstRun(opts = {}) {
75
74
  if (!existsSync(configPath)) {
76
75
  updated = {
77
76
  _help: CONFIG_HELP,
77
+ proxyUrl: "",
78
78
  btpUrl: "",
79
79
  relayUrl: "ws://localhost:7100",
80
80
  ...updated
81
81
  };
82
82
  changed = true;
83
83
  log(
84
- `[toon-clientd] wrote starter config at ${configPath} \u2014 set "btpUrl" to your apex before publishing.`
84
+ `[toon-clientd] wrote starter config at ${configPath} \u2014 set "proxyUrl" (connector proxy, ILP-over-HTTP) or "btpUrl" (BTP) to your apex before publishing.`
85
85
  );
86
86
  }
87
87
  if (changed) writeConfigFile(configPath, updated);
@@ -124,7 +124,6 @@ var DEFAULT_MAX_MS = 3e4;
124
124
  var noop = () => void 0;
125
125
  var RelaySubscription = class {
126
126
  relayUrl;
127
- socksProxy;
128
127
  bufferSize;
129
128
  reconnectBaseMs;
130
129
  reconnectMaxMs;
@@ -147,12 +146,11 @@ var RelaySubscription = class {
147
146
  reconnectTimer = null;
148
147
  constructor(opts) {
149
148
  this.relayUrl = opts.relayUrl;
150
- this.socksProxy = opts.socksProxy;
151
149
  this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;
152
150
  this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;
153
151
  this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;
154
152
  this.log = opts.logger ?? noop;
155
- this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
153
+ this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory();
156
154
  this.decodeEvent = opts.decodeEvent;
157
155
  this.onEvent = opts.onEvent;
158
156
  }
@@ -255,7 +253,7 @@ var RelaySubscription = class {
255
253
  }
256
254
  scheduleReconnect() {
257
255
  if (this.closing || this.reconnectTimer) return;
258
- const delay2 = Math.min(
256
+ const delay3 = Math.min(
259
257
  this.reconnectMaxMs,
260
258
  this.reconnectBaseMs * 2 ** this.reconnectAttempts
261
259
  );
@@ -263,7 +261,7 @@ var RelaySubscription = class {
263
261
  this.reconnectTimer = setTimeout(() => {
264
262
  this.reconnectTimer = null;
265
263
  if (!this.closing) this.open();
266
- }, delay2);
264
+ }, delay3);
267
265
  this.reconnectTimer.unref?.();
268
266
  }
269
267
  sendReq(subId, filters) {
@@ -347,15 +345,10 @@ function toText(data) {
347
345
  function errMsg(err) {
348
346
  return err instanceof Error ? err.message : String(err);
349
347
  }
350
- function defaultWebSocketFactory(socksProxy) {
348
+ function defaultWebSocketFactory() {
351
349
  return (url) => {
352
350
  const WebSocketImpl = nodeRequire("ws");
353
- let agent;
354
- if (socksProxy) {
355
- const { SocksProxyAgent } = nodeRequire("socks-proxy-agent");
356
- agent = new SocksProxyAgent(socksProxy);
357
- }
358
- return new WebSocketImpl(url, agent ? { agent } : void 0);
351
+ return new WebSocketImpl(url);
359
352
  };
360
353
  }
361
354
 
@@ -1304,6 +1297,7 @@ var ApexDiscoveryError = class extends Error {
1304
1297
  this.retryable = retryable;
1305
1298
  this.name = "ApexDiscoveryError";
1306
1299
  }
1300
+ retryable;
1307
1301
  };
1308
1302
  async function discoverApex(params) {
1309
1303
  const { relay, ilpAddress, pubkey, chain, childPeers } = params;
@@ -1403,7 +1397,6 @@ var ClientRunner = class {
1403
1397
  config;
1404
1398
  createClient;
1405
1399
  createRelay;
1406
- startReadProxy;
1407
1400
  log;
1408
1401
  targetsPath;
1409
1402
  startedAt = Date.now();
@@ -1423,9 +1416,6 @@ var ClientRunner = class {
1423
1416
  subIdCounter = 0;
1424
1417
  defaultBtpUrl;
1425
1418
  defaultRelayUrl;
1426
- /** Teardown for the daemon-managed read proxy (btp-direct + relay-`.anyone`). */
1427
- stopReadProxy;
1428
- readProxyError;
1429
1419
  stopped = false;
1430
1420
  started = false;
1431
1421
  constructor(deps) {
@@ -1437,17 +1427,12 @@ var ClientRunner = class {
1437
1427
  this.defaultRelayUrl = deps.config.relayUrl;
1438
1428
  this.createRelay = deps.createRelay ?? ((opts) => new RelaySubscription({
1439
1429
  relayUrl: opts.relayUrl,
1440
- ...opts.socksProxy ? { socksProxy: opts.socksProxy } : {},
1441
1430
  ...opts.logger ? { logger: opts.logger } : {},
1442
1431
  onEvent: opts.onEvent,
1443
1432
  // The TOON relay sends events TOON-encoded (text) on reads, not JSON.
1444
1433
  decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
1445
1434
  }));
1446
- this.startReadProxy = deps.startReadProxy ?? ((opts) => startManagedAnonProxy({
1447
- socksPort: opts.socksPort,
1448
- ...opts.log ? { log: opts.log } : {}
1449
- }));
1450
- this.registerRelay(this.defaultRelayUrl, this.config.socksProxy);
1435
+ this.registerRelay(this.defaultRelayUrl);
1451
1436
  const defaultApex = this.makeApex({
1452
1437
  btpUrl: this.defaultBtpUrl,
1453
1438
  client: this.createClient(this.config.toonClientConfig),
@@ -1469,13 +1454,13 @@ var ClientRunner = class {
1469
1454
  start() {
1470
1455
  if (this.started) return;
1471
1456
  this.started = true;
1472
- if (this.config.manageReadProxy) void this.bringUpReadProxy();
1473
1457
  for (const relay of this.relays.values()) relay.start();
1474
1458
  void this.bootstrap();
1475
1459
  this.replayPersistedTargets();
1476
1460
  }
1477
1461
  /** Await the default apex's bootstrap (kicking it off if not already running). */
1478
1462
  bootstrap() {
1463
+ if (!this.config.hasUplink) return Promise.resolve();
1479
1464
  const apex = this.apexes.get(this.defaultBtpUrl);
1480
1465
  if (!apex) return Promise.resolve();
1481
1466
  return this.bootstrapApex(apex);
@@ -1486,12 +1471,11 @@ var ClientRunner = class {
1486
1471
  * merged buffer and replaying active fan-out subscriptions. Does NOT start the
1487
1472
  * socket — callers start it (so construction stays side-effect-free for tests).
1488
1473
  */
1489
- registerRelay(relayUrl, socksProxy) {
1474
+ registerRelay(relayUrl) {
1490
1475
  const existing = this.relays.get(relayUrl);
1491
1476
  if (existing) return existing;
1492
1477
  const relay = this.createRelay({
1493
1478
  relayUrl,
1494
- ...socksProxy ? { socksProxy } : {},
1495
1479
  logger: this.log,
1496
1480
  onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event)
1497
1481
  });
@@ -1501,17 +1485,11 @@ var ClientRunner = class {
1501
1485
  return relay;
1502
1486
  }
1503
1487
  /**
1504
- * Add a relay read target at runtime. `.anyone` relays reuse the managed read
1505
- * proxy (started here if needed). Persisted unless `persist` is false.
1488
+ * Add a relay read target at runtime. Persisted unless `persist` is false.
1506
1489
  */
1507
1490
  async addRelay(relayUrl, persist = true) {
1508
1491
  if (this.relays.has(relayUrl)) return;
1509
- let socksProxy = this.config.socksProxy;
1510
- if (isAnyoneHost(relayUrl) && !socksProxy) {
1511
- await this.ensureReadProxy();
1512
- socksProxy = `socks5h://127.0.0.1:${this.config.readProxySocksPort ?? 9050}`;
1513
- }
1514
- const relay = this.registerRelay(relayUrl, socksProxy);
1492
+ const relay = this.registerRelay(relayUrl);
1515
1493
  relay.start();
1516
1494
  if (persist) saveRelayTarget(relayUrl, this.targetsPath);
1517
1495
  }
@@ -1558,6 +1536,24 @@ var ClientRunner = class {
1558
1536
  for (const url of targets) this.relays.get(url)?.subscribe(filters, subId);
1559
1537
  return { subId, relays: targets };
1560
1538
  }
1539
+ /**
1540
+ * One-shot free read: subscribe the given filter(s) across all relays, wait a
1541
+ * bounded window for the relay(s) to deliver, then return every buffered event
1542
+ * matching the filter (matched by content, not subId — so events already
1543
+ * buffered by other subscriptions are included despite the global dedup).
1544
+ *
1545
+ * Backs the apps `toon_query` tool the generative-UI runtime calls to resolve
1546
+ * a ViewSpec node's data bind.
1547
+ */
1548
+ async query(filters, timeoutMs = 1200) {
1549
+ const list = Array.isArray(filters) ? filters : [filters];
1550
+ const subId = `q-${++this.subIdCounter}`;
1551
+ const targets = [...this.relays.keys()];
1552
+ for (const url of targets) this.relays.get(url)?.subscribe(list, subId);
1553
+ await delay2(timeoutMs);
1554
+ for (const url of targets) this.relays.get(url)?.unsubscribe(subId);
1555
+ return this.merged.map((m) => m.event).filter((event) => list.some((f) => matchesFilter(event, f)));
1556
+ }
1561
1557
  /** Drain merged events newer than the cursor (free read), optionally scoped. */
1562
1558
  getEvents(query) {
1563
1559
  const after = query.cursor ?? 0;
@@ -1597,14 +1593,20 @@ var ClientRunner = class {
1597
1593
  async doBootstrapApex(apex) {
1598
1594
  apex.bootstrapping = true;
1599
1595
  try {
1596
+ if (!apex.negotiation && this.config.proxyUrl) {
1597
+ await this.discoverApexNegotiation(apex);
1598
+ }
1600
1599
  await apex.client.start();
1601
1600
  this.injectApexNegotiation(apex);
1602
- apex.apexChannelId = await this.openOrResumeApexChannel(apex);
1601
+ const deferOpen = Boolean(this.config.proxyUrl);
1602
+ apex.apexChannelId = await this.openOrResumeApexChannel(apex, {
1603
+ resumeOnly: deferOpen
1604
+ });
1603
1605
  this.routeChildPeersThroughApexChannel(apex);
1604
1606
  apex.ready = true;
1605
1607
  apex.lastError = void 0;
1606
1608
  this.log(
1607
- `[runner] apex ${apex.btpUrl} ready; channel ${apex.apexChannelId}`
1609
+ `[runner] apex ${apex.btpUrl || apex.destination} ready; channel ${apex.apexChannelId ?? "(deferred \u2014 open on first write)"}`
1608
1610
  );
1609
1611
  } catch (err) {
1610
1612
  apex.lastError = err instanceof Error ? err.message : String(err);
@@ -1733,37 +1735,15 @@ var ClientRunner = class {
1733
1735
  );
1734
1736
  }
1735
1737
  }
1736
- // ── Shared read proxy ──────────────────────────────────────────────────────
1737
- async ensureReadProxy() {
1738
- if (this.stopReadProxy) return;
1739
- await this.bringUpReadProxy();
1740
- }
1741
- async bringUpReadProxy() {
1742
- if (this.stopReadProxy) return;
1743
- const socksPort = this.config.readProxySocksPort ?? 9050;
1744
- try {
1745
- this.log(
1746
- `[runner] starting managed read proxy on 127.0.0.1:${socksPort}`
1747
- );
1748
- const proxy = await this.startReadProxy({
1749
- socksPort,
1750
- log: (m) => this.log(`[anon] ${m}`)
1751
- });
1752
- if (this.stopped) {
1753
- await proxy.stop();
1754
- return;
1755
- }
1756
- this.stopReadProxy = () => proxy.stop();
1757
- this.readProxyError = void 0;
1758
- this.log("[runner] managed read proxy ready");
1759
- } catch (err) {
1760
- this.readProxyError = err instanceof Error ? err.message : String(err);
1761
- this.log(`[runner] managed read proxy failed: ${this.readProxyError}`);
1762
- }
1763
- }
1764
1738
  // ── Channel / negotiation helpers (per-apex) ───────────────────────────────
1765
- /** Open the apex channel — or, on a restart, RESUME the existing one. */
1766
- async openOrResumeApexChannel(apex) {
1739
+ /**
1740
+ * Open the apex channel — or, on a restart, RESUME the existing one.
1741
+ *
1742
+ * With `resumeOnly`, only a persisted channel is resumed (no on-chain open);
1743
+ * returns undefined when none exists so the caller can defer the open to the
1744
+ * first write (funded-after-start demo flow, #69).
1745
+ */
1746
+ async openOrResumeApexChannel(apex, opts = {}) {
1767
1747
  const { destination, chain } = apex;
1768
1748
  const { apexChannelStorePath } = this.config;
1769
1749
  const saved = loadApexChannel(apexChannelStorePath, destination, chain);
@@ -1775,10 +1755,24 @@ var ClientRunner = class {
1775
1755
  );
1776
1756
  return saved.channelId;
1777
1757
  }
1758
+ if (opts.resumeOnly) return void 0;
1778
1759
  const channelId = await apex.client.openChannel(destination);
1779
- if (apex.negotiation) {
1780
- const a = apex.negotiation;
1781
- saveApexChannel(apexChannelStorePath, destination, chain, {
1760
+ this.persistApexChannel(apex, channelId);
1761
+ return channelId;
1762
+ }
1763
+ /**
1764
+ * Persist a (lazily- or eagerly-) opened apex channel so a restart RESUMES it
1765
+ * (tracked, no re-deposit) rather than opening a second on-chain channel.
1766
+ * No-op when the apex carries no negotiation (nothing to key the store on).
1767
+ */
1768
+ persistApexChannel(apex, channelId) {
1769
+ const a = apex.negotiation;
1770
+ if (!a) return;
1771
+ saveApexChannel(
1772
+ this.config.apexChannelStorePath,
1773
+ apex.destination,
1774
+ apex.chain,
1775
+ {
1782
1776
  channelId,
1783
1777
  context: {
1784
1778
  chainType: a.chain,
@@ -1787,9 +1781,34 @@ var ClientRunner = class {
1787
1781
  ...a.tokenAddress ? { tokenAddress: a.tokenAddress } : {},
1788
1782
  recipient: a.settlementAddress
1789
1783
  }
1790
- });
1784
+ }
1785
+ );
1786
+ }
1787
+ /**
1788
+ * Discover the apex's settlement negotiation from its kind:10032 on the
1789
+ * default relay and attach it to the apex (proxy-mode fallback when no config
1790
+ * negotiation was supplied, #69). Throws ApexDiscoveryError on timeout/missing
1791
+ * settlement params so the apex's `lastError` reports exactly what is missing.
1792
+ */
1793
+ async discoverApexNegotiation(apex) {
1794
+ const relay = this.relays.get(this.defaultRelayUrl);
1795
+ if (!relay) {
1796
+ throw new TargetError(
1797
+ `Cannot discover apex "${apex.destination}": default relay ${this.defaultRelayUrl} is not registered.`
1798
+ );
1791
1799
  }
1792
- return channelId;
1800
+ relay.start();
1801
+ const discovered = await discoverApex({
1802
+ relay,
1803
+ ilpAddress: apex.destination,
1804
+ chain: apex.chain,
1805
+ ...apex.childPeers.length > 0 ? { childPeers: apex.childPeers } : {}
1806
+ });
1807
+ apex.negotiation = discovered.negotiation;
1808
+ if (discovered.apexChildPeers) apex.childPeers = discovered.apexChildPeers;
1809
+ this.log(
1810
+ `[runner] discovered apex negotiation for "${apex.destination}" (chain ${discovered.negotiation.chainKey}, settle ${discovered.negotiation.settlementAddress})`
1811
+ );
1793
1812
  }
1794
1813
  /** Inject the apex settlement negotiation directly into its ToonClient. */
1795
1814
  injectApexNegotiation(apex) {
@@ -1873,16 +1892,14 @@ var ClientRunner = class {
1873
1892
  minaAddress: safe(() => client?.getMinaAddress())
1874
1893
  },
1875
1894
  transport: {
1876
- type: this.config.socksProxy ? "socks5" : "direct",
1877
- ...this.config.socksProxy ? { socksProxy: this.config.socksProxy } : {},
1895
+ type: "direct",
1878
1896
  ...apex ? { btpUrl: apex.btpUrl } : {}
1879
1897
  },
1880
1898
  relay: {
1881
1899
  url: this.defaultRelayUrl,
1882
1900
  connected: relay?.isConnected() ?? false,
1883
1901
  buffered: relay?.bufferedCount() ?? 0,
1884
- subscriptions: relay?.activeSubscriptions() ?? [],
1885
- ...this.readProxyError ? { proxyError: this.readProxyError } : {}
1902
+ subscriptions: relay?.activeSubscriptions() ?? []
1886
1903
  },
1887
1904
  ...network ? { network } : {},
1888
1905
  ...apex?.lastError ? { lastError: apex.lastError } : {}
@@ -1916,7 +1933,14 @@ var ClientRunner = class {
1916
1933
  async publish(req) {
1917
1934
  const apex = this.selectApex(req.btpUrl);
1918
1935
  this.assertApexReady(apex);
1919
- const channelId = apex.apexChannelId ?? await apex.client.openChannel(req.destination);
1936
+ let channelId = apex.apexChannelId;
1937
+ if (!channelId) {
1938
+ channelId = await apex.client.openChannel(req.destination);
1939
+ if (!req.destination || req.destination === apex.destination) {
1940
+ apex.apexChannelId = channelId;
1941
+ this.persistApexChannel(apex, channelId);
1942
+ }
1943
+ }
1920
1944
  const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
1921
1945
  const claim = await apex.client.signBalanceProof(channelId, fee);
1922
1946
  const result = await apex.client.publishEvent(req.event, {
@@ -1934,6 +1958,104 @@ var ClientRunner = class {
1934
1958
  nonce: apex.client.getChannelNonce(channelId)
1935
1959
  };
1936
1960
  }
1961
+ /**
1962
+ * Build, sign (with the daemon-held key), and pay-to-write an event. The
1963
+ * caller supplies only the event shell; the private key never leaves the
1964
+ * daemon. Payloads are MODEL-AUTHORED → validated server-side here (the model
1965
+ * is not a security boundary). Replaceable kinds (0/3) merge the latest known
1966
+ * event's tags before signing.
1967
+ */
1968
+ async publishUnsigned(req) {
1969
+ const apex = this.selectApex(req.btpUrl);
1970
+ this.assertApexReady(apex);
1971
+ const template = this.buildTemplate(apex, req);
1972
+ const signed = await apex.client.signEvent(template);
1973
+ return this.publish({
1974
+ event: signed,
1975
+ ...req.destination ? { destination: req.destination } : {},
1976
+ ...req.fee ? { fee: req.fee } : {},
1977
+ ...req.btpUrl ? { btpUrl: req.btpUrl } : {}
1978
+ });
1979
+ }
1980
+ /**
1981
+ * Upload media to Arweave (kind:5094 blob DVM, single-packet) then sign+publish
1982
+ * a media event referencing the resulting URL. One spendy operation, two steps,
1983
+ * entirely server-side.
1984
+ */
1985
+ async uploadMedia(req) {
1986
+ const apex = this.selectApex(req.btpUrl);
1987
+ this.assertApexReady(apex);
1988
+ if (typeof req.dataBase64 !== "string" || req.dataBase64 === "") {
1989
+ throw new InvalidPayloadError("dataBase64 (base64 media bytes) is required.");
1990
+ }
1991
+ const blobData = new Uint8Array(Buffer.from(req.dataBase64, "base64"));
1992
+ const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
1993
+ const upload = await apex.client.uploadBlob({
1994
+ blobData,
1995
+ ...req.mime ? { contentType: req.mime } : {},
1996
+ ilpAmount: fee
1997
+ });
1998
+ if (!upload.success || !upload.txId) {
1999
+ throw new PublishRejectedError(upload.error ?? "blob upload rejected");
2000
+ }
2001
+ const url = `${ARWEAVE_GATEWAY}/${upload.txId}`;
2002
+ const kind = req.kind ?? 1063;
2003
+ const signed = await apex.client.signEvent({
2004
+ kind,
2005
+ created_at: nowSeconds(),
2006
+ tags: this.buildMediaTags(kind, url, req),
2007
+ content: req.caption ?? ""
2008
+ });
2009
+ const pub = await this.publish({
2010
+ event: signed,
2011
+ ...req.fee ? { fee: req.fee } : {},
2012
+ ...req.btpUrl ? { btpUrl: req.btpUrl } : {}
2013
+ });
2014
+ return { ...pub, url, txId: upload.txId };
2015
+ }
2016
+ /** Validate + assemble a signable event template (with replaceable merge). */
2017
+ buildTemplate(apex, req) {
2018
+ if (!Number.isInteger(req.kind) || req.kind < 0 || req.kind > 65535) {
2019
+ throw new InvalidPayloadError("kind must be an integer in [0, 65535].");
2020
+ }
2021
+ if (req.content !== void 0 && typeof req.content !== "string") {
2022
+ throw new InvalidPayloadError("content must be a string.");
2023
+ }
2024
+ const tags = normalizeTags(req.tags);
2025
+ const content = req.content ?? "";
2026
+ if (req.kind === 0 || req.kind === 3) {
2027
+ const prior = this.latestSelfReplaceable(apex, req.kind);
2028
+ if (prior) {
2029
+ return {
2030
+ kind: req.kind,
2031
+ created_at: nowSeconds(),
2032
+ tags: mergeTags(prior.tags, tags),
2033
+ content: content !== "" ? content : prior.content
2034
+ };
2035
+ }
2036
+ }
2037
+ return { kind: req.kind, created_at: nowSeconds(), tags, content };
2038
+ }
2039
+ /** Latest self-authored event of `kind` currently in the merged read buffer. */
2040
+ latestSelfReplaceable(apex, kind) {
2041
+ const pubkey = safe(() => apex.client.getPublicKey());
2042
+ if (!pubkey) return void 0;
2043
+ let latest;
2044
+ for (const m of this.merged) {
2045
+ if (m.event.kind !== kind || m.event.pubkey !== pubkey) continue;
2046
+ if (!latest || m.event.created_at > latest.created_at) latest = m.event;
2047
+ }
2048
+ return latest;
2049
+ }
2050
+ /** Tags for a published media event referencing an Arweave URL. */
2051
+ buildMediaTags(kind, url, req) {
2052
+ const mime = req.mime ?? "application/octet-stream";
2053
+ const extra = normalizeTags(req.tags);
2054
+ if (kind === 1063) {
2055
+ return [["url", url], ["m", mime], ...extra];
2056
+ }
2057
+ return [["imeta", `url ${url}`, `m ${mime}`], ...extra];
2058
+ }
1937
2059
  /** Open (or return) a payment channel on the selected (or default) apex. */
1938
2060
  async openChannel(destination, btpUrl) {
1939
2061
  const apex = this.selectApex(btpUrl);
@@ -1942,7 +2064,9 @@ var ClientRunner = class {
1942
2064
  destination ?? apex.destination
1943
2065
  );
1944
2066
  if (!destination || destination === apex.destination) {
2067
+ const firstOpen = apex.apexChannelId !== channelId;
1945
2068
  apex.apexChannelId = channelId;
2069
+ if (firstOpen) this.persistApexChannel(apex, channelId);
1946
2070
  }
1947
2071
  return { channelId };
1948
2072
  }
@@ -1999,19 +2123,31 @@ var ClientRunner = class {
1999
2123
  ...firstReject ? { code: firstReject.code, message: firstReject.message } : {}
2000
2124
  };
2001
2125
  }
2002
- /** Graceful teardown: close every relay + stop every apex client + read proxy. */
2126
+ /**
2127
+ * Payment-aware HTTP fetch through an apex's client. The client issues the
2128
+ * request and, on `402 Payment Required`, pays over TOON and retries; we
2129
+ * translate the resulting Web `Response` into the wire envelope.
2130
+ */
2131
+ async httpFetchPaid(req) {
2132
+ const apex = this.selectApex();
2133
+ this.assertApexReady(apex);
2134
+ const res = await apex.client.h402Fetch(req.url, {
2135
+ ...req.method ? { method: req.method } : {},
2136
+ ...req.headers ? { headers: req.headers } : {},
2137
+ ...req.body !== void 0 ? { body: req.body } : {},
2138
+ ...req.timeout !== void 0 ? { timeout: req.timeout } : {}
2139
+ });
2140
+ return {
2141
+ status: res.status,
2142
+ headers: Object.fromEntries(res.headers.entries()),
2143
+ body: await res.text()
2144
+ };
2145
+ }
2146
+ /** Graceful teardown: close every relay + stop every apex client. */
2003
2147
  async stop() {
2004
2148
  if (this.stopped) return;
2005
2149
  this.stopped = true;
2006
2150
  for (const relay of this.relays.values()) relay.close();
2007
- if (this.stopReadProxy) {
2008
- try {
2009
- await this.stopReadProxy();
2010
- } catch (err) {
2011
- this.log(`[runner] read proxy stop error: ${errMsg2(err)}`);
2012
- }
2013
- this.stopReadProxy = void 0;
2014
- }
2015
2151
  for (const apex of this.apexes.values()) {
2016
2152
  try {
2017
2153
  await apex.client.stop();
@@ -2032,9 +2168,14 @@ var ClientRunner = class {
2032
2168
  return def;
2033
2169
  }
2034
2170
  assertApexReady(apex) {
2171
+ if (!this.config.hasUplink) {
2172
+ throw new TargetError(
2173
+ "No write uplink configured \u2014 this daemon is read-only. Set TOON_CLIENT_PROXY_URL (connector proxy) or TOON_CLIENT_BTP_URL to enable paid writes."
2174
+ );
2175
+ }
2035
2176
  if (!apex.ready) {
2036
2177
  throw new NotReadyError(
2037
- apex.bootstrapping ? "Apex is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
2178
+ apex.bootstrapping ? "Apex is still bootstrapping (transport/channel coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
2038
2179
  );
2039
2180
  }
2040
2181
  }
@@ -2058,6 +2199,57 @@ var TargetError = class extends Error {
2058
2199
  this.name = "TargetError";
2059
2200
  }
2060
2201
  };
2202
+ var InvalidPayloadError = class extends Error {
2203
+ constructor(message) {
2204
+ super(message);
2205
+ this.name = "InvalidPayloadError";
2206
+ }
2207
+ };
2208
+ var ARWEAVE_GATEWAY = "https://arweave.net";
2209
+ function nowSeconds() {
2210
+ return Math.floor(Date.now() / 1e3);
2211
+ }
2212
+ function delay2(ms) {
2213
+ return new Promise((resolve) => setTimeout(resolve, ms));
2214
+ }
2215
+ function matchesFilter(event, filter) {
2216
+ if (filter.ids && !filter.ids.includes(event.id)) return false;
2217
+ if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
2218
+ if (filter.authors && !filter.authors.includes(event.pubkey)) return false;
2219
+ if (filter.since !== void 0 && event.created_at < filter.since) return false;
2220
+ if (filter.until !== void 0 && event.created_at > filter.until) return false;
2221
+ for (const [key2, values] of Object.entries(filter)) {
2222
+ if (!key2.startsWith("#") || !Array.isArray(values)) continue;
2223
+ const letter = key2.slice(1);
2224
+ const hit = event.tags.some(
2225
+ (t) => t[0] === letter && t[1] !== void 0 && values.includes(t[1])
2226
+ );
2227
+ if (!hit) return false;
2228
+ }
2229
+ return true;
2230
+ }
2231
+ function normalizeTags(raw) {
2232
+ if (raw === void 0) return [];
2233
+ if (!Array.isArray(raw)) throw new InvalidPayloadError("tags must be an array.");
2234
+ return raw.map((tag, i) => {
2235
+ if (!Array.isArray(tag) || !tag.every((x) => typeof x === "string")) {
2236
+ throw new InvalidPayloadError(`tags[${i}] must be an array of strings.`);
2237
+ }
2238
+ return tag;
2239
+ });
2240
+ }
2241
+ function mergeTags(base, additions) {
2242
+ const seen = new Set(base.map((t) => JSON.stringify(t)));
2243
+ const out = [...base];
2244
+ for (const tag of additions) {
2245
+ const key2 = JSON.stringify(tag);
2246
+ if (!seen.has(key2)) {
2247
+ seen.add(key2);
2248
+ out.push(tag);
2249
+ }
2250
+ }
2251
+ return out;
2252
+ }
2061
2253
  function safe(fn) {
2062
2254
  try {
2063
2255
  return fn();
@@ -2071,13 +2263,6 @@ function errMsg2(err) {
2071
2263
  function sanitize(s) {
2072
2264
  return s.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
2073
2265
  }
2074
- function isAnyoneHost(url) {
2075
- try {
2076
- return new URL(url).hostname.endsWith(".anyone");
2077
- } catch {
2078
- return false;
2079
- }
2080
- }
2081
2266
 
2082
2267
  // src/daemon/routes.ts
2083
2268
  function registerRoutes(app, runner) {
@@ -2095,6 +2280,49 @@ function registerRoutes(app, runner) {
2095
2280
  return mapError(reply, err);
2096
2281
  }
2097
2282
  });
2283
+ app.post(
2284
+ "/publish-unsigned",
2285
+ async (req, reply) => {
2286
+ const body = req.body;
2287
+ if (!body || !Number.isInteger(body.kind)) {
2288
+ return sendError(reply, 400, "invalid_event", {
2289
+ detail: "body.kind (integer) is required; the daemon signs the event."
2290
+ });
2291
+ }
2292
+ try {
2293
+ return await runner.publishUnsigned(body);
2294
+ } catch (err) {
2295
+ return mapError(reply, err);
2296
+ }
2297
+ }
2298
+ );
2299
+ app.post("/upload-media", async (req, reply) => {
2300
+ const body = req.body;
2301
+ if (!body || typeof body.dataBase64 !== "string" || body.dataBase64 === "") {
2302
+ return sendError(reply, 400, "invalid_media", {
2303
+ detail: "body.dataBase64 (base64-encoded media bytes) is required."
2304
+ });
2305
+ }
2306
+ try {
2307
+ return await runner.uploadMedia(body);
2308
+ } catch (err) {
2309
+ return mapError(reply, err);
2310
+ }
2311
+ });
2312
+ app.post("/query", async (req, reply) => {
2313
+ const body = req.body;
2314
+ if (!body || body.filters === void 0) {
2315
+ return sendError(reply, 400, "invalid_filters", {
2316
+ detail: "body.filters is required (a NIP-01 filter or array of filters)."
2317
+ });
2318
+ }
2319
+ try {
2320
+ const events = await runner.query(body.filters, body.timeoutMs);
2321
+ return { events };
2322
+ } catch (err) {
2323
+ return mapError(reply, err);
2324
+ }
2325
+ });
2098
2326
  app.post("/subscribe", async (req, reply) => {
2099
2327
  const body = req.body;
2100
2328
  if (!body || body.filters === void 0) {
@@ -2138,6 +2366,22 @@ function registerRoutes(app, runner) {
2138
2366
  return mapError(reply, err);
2139
2367
  }
2140
2368
  });
2369
+ app.post(
2370
+ "/http-fetch-paid",
2371
+ async (req, reply) => {
2372
+ const body = req.body;
2373
+ if (!body || typeof body.url !== "string" || body.url === "") {
2374
+ return sendError(reply, 400, "invalid_url", {
2375
+ detail: "body.url (absolute resource URL) is required."
2376
+ });
2377
+ }
2378
+ try {
2379
+ return await runner.httpFetchPaid(body);
2380
+ } catch (err) {
2381
+ return mapError(reply, err);
2382
+ }
2383
+ }
2384
+ );
2141
2385
  app.get("/targets", async () => runner.getTargets());
2142
2386
  app.post("/relays", async (req, reply) => {
2143
2387
  const url = req.body?.relayUrl;
@@ -2207,6 +2451,9 @@ function mapError(reply, err) {
2207
2451
  retryable: true
2208
2452
  });
2209
2453
  }
2454
+ if (err instanceof InvalidPayloadError) {
2455
+ return sendError(reply, 400, "invalid_payload", { detail: err.message });
2456
+ }
2210
2457
  if (err instanceof PublishRejectedError) {
2211
2458
  return sendError(reply, 502, "rejected", { detail: err.message });
2212
2459
  }
@@ -2242,4 +2489,4 @@ export {
2242
2489
  PublishRejectedError,
2243
2490
  registerRoutes
2244
2491
  };
2245
- //# sourceMappingURL=chunk-3NAWISI5.js.map
2492
+ //# sourceMappingURL=chunk-QFHCXJ2V.js.map