@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.
- package/README.md +19 -38
- package/dist/app/index.html +166 -0
- package/dist/{chunk-5KFXUT5Q.js → chunk-CQ2QIZ6Z.js} +864 -604
- package/dist/chunk-CQ2QIZ6Z.js.map +1 -0
- package/dist/{chunk-FSS45ZX3.js → chunk-DLYE6U2Z.js} +1516 -316
- package/dist/chunk-DLYE6U2Z.js.map +1 -0
- package/dist/{chunk-3NAWISI5.js → chunk-QFHCXJ2V.js} +348 -101
- package/dist/chunk-QFHCXJ2V.js.map +1 -0
- package/dist/chunk-XV52IHVR.js +977 -0
- package/dist/chunk-XV52IHVR.js.map +1 -0
- package/dist/daemon.js +3 -4
- package/dist/daemon.js.map +1 -1
- package/dist/e2e/run-journey.d.ts +1 -0
- package/dist/e2e/run-journey.js +19953 -0
- package/dist/e2e/run-journey.js.map +1 -0
- package/dist/{ed25519-2QVPINLS.js → ed25519-2LFQXLYS.js} +6 -2
- package/dist/index.d.ts +282 -68
- package/dist/index.js +380 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +39 -6
- package/dist/mcp.js.map +1 -1
- package/package.json +11 -7
- package/dist/anon-proxy-W3KMM7GU-FN7ZJY7P.js +0 -25
- package/dist/chunk-3NAWISI5.js.map +0 -1
- package/dist/chunk-5KFXUT5Q.js.map +0 -1
- package/dist/chunk-FSS45ZX3.js.map +0 -1
- package/dist/chunk-SKQTKZIH.js +0 -278
- package/dist/chunk-SKQTKZIH.js.map +0 -1
- package/dist/chunk-ZQKYZJWT.js +0 -359
- package/dist/chunk-ZQKYZJWT.js.map +0 -1
- package/dist/ed25519-2QVPINLS.js.map +0 -1
- package/dist/gateway-QOK47RKS-SEGTXBR3.js +0 -16
- package/dist/gateway-QOK47RKS-SEGTXBR3.js.map +0 -1
- package/dist/socks5-WTJBYGME-6COK4LXW.js +0 -139
- package/dist/socks5-WTJBYGME-6COK4LXW.js.map +0 -1
- /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-
|
|
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: "
|
|
37
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
},
|
|
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(
|
|
348
|
+
function defaultWebSocketFactory() {
|
|
351
349
|
return (url) => {
|
|
352
350
|
const WebSocketImpl = nodeRequire("ws");
|
|
353
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
1766
|
-
|
|
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
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
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-
|
|
2492
|
+
//# sourceMappingURL=chunk-QFHCXJ2V.js.map
|