@tokenbuddy/tokenbuddy 1.0.4

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 (43) hide show
  1. package/bin/tb-proxyd.js +2 -0
  2. package/bin/tb.js +3 -0
  3. package/bin/tokenbuddy-proxyd.js +2 -0
  4. package/bin/tokenbuddy.js +3 -0
  5. package/dist/src/buyer-store.d.ts +118 -0
  6. package/dist/src/buyer-store.d.ts.map +1 -0
  7. package/dist/src/buyer-store.js +296 -0
  8. package/dist/src/buyer-store.js.map +1 -0
  9. package/dist/src/cli.d.ts +3 -0
  10. package/dist/src/cli.d.ts.map +1 -0
  11. package/dist/src/cli.js +648 -0
  12. package/dist/src/cli.js.map +1 -0
  13. package/dist/src/daemon.d.ts +48 -0
  14. package/dist/src/daemon.d.ts.map +1 -0
  15. package/dist/src/daemon.js +998 -0
  16. package/dist/src/daemon.js.map +1 -0
  17. package/dist/src/index.d.ts +2 -0
  18. package/dist/src/index.d.ts.map +1 -0
  19. package/dist/src/index.js +12 -0
  20. package/dist/src/index.js.map +1 -0
  21. package/dist/src/provider-install.d.ts +44 -0
  22. package/dist/src/provider-install.d.ts.map +1 -0
  23. package/dist/src/provider-install.js +286 -0
  24. package/dist/src/provider-install.js.map +1 -0
  25. package/dist/src/tb-proxyd.d.ts +2 -0
  26. package/dist/src/tb-proxyd.d.ts.map +1 -0
  27. package/dist/src/tb-proxyd.js +54 -0
  28. package/dist/src/tb-proxyd.js.map +1 -0
  29. package/dist/src/terminal-detect.d.ts +29 -0
  30. package/dist/src/terminal-detect.d.ts.map +1 -0
  31. package/dist/src/terminal-detect.js +209 -0
  32. package/dist/src/terminal-detect.js.map +1 -0
  33. package/package.json +29 -0
  34. package/src/buyer-store.ts +536 -0
  35. package/src/cli.ts +732 -0
  36. package/src/daemon.ts +1158 -0
  37. package/src/index.ts +12 -0
  38. package/src/provider-install.ts +363 -0
  39. package/src/tb-proxyd.ts +60 -0
  40. package/src/terminal-detect.ts +225 -0
  41. package/tests/e2e.test.ts +264 -0
  42. package/tests/tokenbuddy.test.ts +1186 -0
  43. package/tsconfig.json +8 -0
@@ -0,0 +1,264 @@
1
+ import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
2
+ import { buildSellerApp } from "../../seller/src/server.js";
3
+ import { TokenbuddyDaemon } from "../src/daemon.js";
4
+ import { AdminClient } from "../../admin-cli/src/client.js";
5
+ import * as path from "path";
6
+ import * as fs from "fs";
7
+ import http from "http";
8
+ import { AddressInfo } from "net";
9
+
10
+ const TEMP_DIR = path.resolve(__dirname, "../../data-test");
11
+ const TEMP_BOOTSTRAP_JSON = path.join(TEMP_DIR, "sellers-e2e.json");
12
+ const TEMP_SELLER_DB = path.join(TEMP_DIR, "seller-e2e.db");
13
+ const TEMP_BUYER_DB = path.join(TEMP_DIR, "buyer-e2e.db");
14
+
15
+ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
16
+ let bootstrapServer: http.Server;
17
+ let sellerServer: http.Server;
18
+ let upstreamServer: http.Server;
19
+ let daemon: TokenbuddyDaemon;
20
+
21
+ let bootstrapPort: number;
22
+ let sellerPort: number;
23
+ let upstreamPort: number;
24
+ let daemonControlPort: number;
25
+ let daemonProxyPort: number;
26
+
27
+ let sellerCloseFn: () => void;
28
+
29
+ beforeAll(async () => {
30
+ // Ensure clean temp dir
31
+ if (!fs.existsSync(TEMP_DIR)) {
32
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
33
+ }
34
+ fs.writeFileSync(TEMP_BOOTSTRAP_JSON, JSON.stringify({ version: 1, sellers: [] }), "utf8");
35
+
36
+ if (fs.existsSync(TEMP_SELLER_DB)) {
37
+ try { fs.unlinkSync(TEMP_SELLER_DB); } catch (e) {}
38
+ }
39
+ if (fs.existsSync(TEMP_BUYER_DB)) {
40
+ try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
41
+ }
42
+
43
+ // 1. Launch Mock Upstream OpenAI completions server
44
+ upstreamServer = http.createServer((req, res) => {
45
+ res.writeHead(200, { "Content-Type": "application/json" });
46
+ if (req.url === "/v1/chat/completions") {
47
+ res.end(JSON.stringify({
48
+ id: "chatcmpl-e2e-ok",
49
+ usage: { prompt_tokens: 20, completion_tokens: 30 },
50
+ choices: [{ message: { role: "assistant", content: "Hello back!" } }]
51
+ }));
52
+ return;
53
+ }
54
+ res.end("{}");
55
+ });
56
+ await new Promise<void>((resolve) => upstreamServer.listen(0, "127.0.0.1", () => resolve()));
57
+ upstreamPort = (upstreamServer.address() as AddressInfo).port;
58
+
59
+ // 2. Launch Wallet Bootstrap server
60
+ const bootstrapApp = buildBootstrapApp({
61
+ payTo: "test-activation-pay-to",
62
+ sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
63
+ skillSlug: "test-activation-slug",
64
+ skillId: "test-activation-id",
65
+ description: "Activate registration",
66
+ resourceUrl: "http://127.0.0.1/verify",
67
+ activationFeeFen: 1,
68
+ microsPerFen: 1000000,
69
+ sellerRegistryPath: TEMP_BOOTSTRAP_JSON,
70
+ operatorSecret: "op-secret",
71
+ allowLocalSellerUrls: true
72
+ });
73
+ bootstrapServer = http.createServer(bootstrapApp);
74
+ await new Promise<void>((resolve) => bootstrapServer.listen(0, "127.0.0.1", () => resolve()));
75
+ bootstrapPort = (bootstrapServer.address() as AddressInfo).port;
76
+
77
+ // 3. Launch Seller server
78
+ const { app: sellerApp, close: closeDb } = buildSellerApp(TEMP_SELLER_DB, {
79
+ allowMock: true,
80
+ upstreamUrl: `http://127.0.0.1:${upstreamPort}`,
81
+ upstreamApiKey: "upstream-mock-key",
82
+ operatorSecret: "op-secret"
83
+ });
84
+ sellerCloseFn = closeDb;
85
+ sellerServer = http.createServer(sellerApp);
86
+ await new Promise<void>((resolve) => sellerServer.listen(0, "127.0.0.1", () => resolve()));
87
+ sellerPort = (sellerServer.address() as AddressInfo).port;
88
+
89
+ // 4. Launch TokenBuddy Buyer Proxy daemon
90
+ // Configure process.env to redirect to this mock seller port in the test environment
91
+ process.env.TOKENBUDDY_TEST_SELLER_URL = `http://127.0.0.1:${sellerPort}`;
92
+
93
+ daemon = new TokenbuddyDaemon({
94
+ controlPort: 0, // dynamic port for control plane
95
+ proxyPort: 0, // dynamic port for proxy plane
96
+ dbPath: TEMP_BUYER_DB,
97
+ sellerRegistryUrl: `http://127.0.0.1:${bootstrapPort}/registry/sellers`
98
+ });
99
+
100
+ daemon.start();
101
+
102
+ // Retrieve active control plane & proxy ports
103
+ // Since we pass 0, the active listeners inside daemon have been dynamically assigned
104
+ // We fetch ports by inspecting process listener fields, or directly reading properties if exposed.
105
+ // In packages/tokenbuddy-cli/src/daemon.ts:
106
+ // this.controlServer = controlApp.listen(this.config.controlPort);
107
+ // this.proxyServer = proxyApp.listen(this.config.proxyPort);
108
+ const daemonCtrl = (daemon as any).controlServer.address() as AddressInfo;
109
+ const daemonProxy = (daemon as any).proxyServer.address() as AddressInfo;
110
+ daemonControlPort = daemonCtrl.port;
111
+ daemonProxyPort = daemonProxy.port;
112
+ });
113
+
114
+ afterAll(async () => {
115
+ // Cleanup servers
116
+ daemon.stop();
117
+ await new Promise<void>((resolve) => sellerServer.close(() => resolve()));
118
+ sellerCloseFn();
119
+ await new Promise<void>((resolve) => bootstrapServer.close(() => resolve()));
120
+ await new Promise<void>((resolve) => upstreamServer.close(() => resolve()));
121
+
122
+ // Delete temporary databases
123
+ if (fs.existsSync(TEMP_BOOTSTRAP_JSON)) {
124
+ try { fs.unlinkSync(TEMP_BOOTSTRAP_JSON); } catch (e) {}
125
+ }
126
+ if (fs.existsSync(TEMP_SELLER_DB)) {
127
+ try { fs.unlinkSync(TEMP_SELLER_DB); } catch (e) {}
128
+ }
129
+ if (fs.existsSync(TEMP_BUYER_DB)) {
130
+ try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
131
+ }
132
+ });
133
+
134
+ test("💡 Verify Full E2E Loop: operator configure -> bootstrap list -> buyer daemon init -> concurrent proxy inference -> precise ledger check", async () => {
135
+ // Step 1: Initialize Admin Client & Verify operator connection
136
+ const admin = new AdminClient(`http://127.0.0.1:${sellerPort}`, "op-secret");
137
+
138
+ const status = await admin.get("/operator/status");
139
+ expect(status.status).toBe("healthy");
140
+
141
+ // Step 2: Operator configures dynamically payment methods
142
+ const clawtipConfig = {
143
+ payTo: "test-merchant-acct",
144
+ sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
145
+ skillSlug: "test-slug",
146
+ skillId: "test-id",
147
+ description: "Token purchase",
148
+ resourceUrl: "http://127.0.0.1/verify"
149
+ };
150
+ const paymentSetRes = await admin.put("/operator/admin/payments/clawtip", clawtipConfig);
151
+ expect(paymentSetRes.status).toBe("success");
152
+
153
+ // Verify payments listed
154
+ const serviceInfo = await admin.get("/operator/admin/service");
155
+ expect(serviceInfo.clawtipConfigured).toBe(true);
156
+
157
+ // Step 3: Register this seller in the wallet bootstrap registry
158
+ const bootstrapAdmin = new AdminClient(`http://127.0.0.1:${bootstrapPort}`, "op-secret");
159
+ const testRegistry = {
160
+ version: 1,
161
+ sellers: [
162
+ {
163
+ id: "seller-e2e-node",
164
+ name: "Seller E2E Node",
165
+ url: `http://127.0.0.1:${sellerPort}`,
166
+ supportedProtocols: ["openai"],
167
+ paymentMethods: ["mock"]
168
+ }
169
+ ]
170
+ };
171
+ const registered = await bootstrapAdmin.put("/operator/registry/sellers", testRegistry);
172
+ expect(registered.sellers.length).toBe(1);
173
+
174
+ // Verify registry list publicly available
175
+ const sellersPublicRes = await fetch(`http://127.0.0.1:${bootstrapPort}/registry/sellers`);
176
+ const sellersPublic = await sellersPublicRes.json() as any;
177
+ expect(sellersPublic.sellers[0].id).toBe("seller-e2e-node");
178
+
179
+ // Step 4: Validate daemon connection and check doctor
180
+ const daemonStatusRes = await fetch(`http://127.0.0.1:${daemonControlPort}/status`);
181
+ const daemonStatus = await daemonStatusRes.json() as any;
182
+ expect(daemonStatus.status).toBe("running");
183
+
184
+ // Step 5: Send concurrent inference chat requests to the buyer proxy plane (Port 17821 proxy equivalent)
185
+ const chatBody = {
186
+ model: "gpt-4",
187
+ messages: [{ role: "user", content: "How is the weather?" }],
188
+ requestId: "e2e_concurrency_req_1"
189
+ };
190
+
191
+ const sendProxyRequest = (reqId: string) => fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({ ...chatBody, requestId: reqId })
195
+ });
196
+
197
+ // Fire 5 concurrent requests representing intense parallel LLM execution by coding terminal (like Claude Code)
198
+ const parallelResponses = await Promise.all([
199
+ sendProxyRequest("req_p_1"),
200
+ sendProxyRequest("req_p_2"),
201
+ sendProxyRequest("req_p_3"),
202
+ sendProxyRequest("req_p_4"),
203
+ sendProxyRequest("req_p_5")
204
+ ]);
205
+
206
+ for (const res of parallelResponses) {
207
+ expect(res.ok).toBe(true);
208
+ const data = await res.json() as any;
209
+ expect(data.id).toBe("chatcmpl-e2e-ok");
210
+ expect(data.choices[0].message.content).toBe("Hello back!");
211
+ }
212
+
213
+ // Step 6: Verify precise seller billing ledger
214
+ // A single token should have been purchased of amount 2,000,000 micros.
215
+ // 5 concurrent requests each used: prompt 20 * 1.0 + completion 30 * 3.0 = 110 micros.
216
+ // Total cost: 5 * 110 = 550 micros.
217
+ // Outstanding remaining balance should be: 2,000,000 - 550 = 1,999,450 micros!
218
+ // Let's connect directly to the seller DB using manual sync query or operator billing list
219
+ const { DatabaseSync } = require("node:sqlite");
220
+ const verifyDb = new DatabaseSync(TEMP_SELLER_DB);
221
+ const credRow = verifyDb.prepare("SELECT credit_balance_micros, reserved_micros, spent_micros FROM credentials").get() as any;
222
+
223
+ expect(credRow.credit_balance_micros).toBe(1999450);
224
+ expect(credRow.reserved_micros).toBe(0);
225
+ expect(credRow.spent_micros).toBe(550);
226
+
227
+ // Also assert 5 distinct request settlement entries exist in ledger
228
+ const requestRows = verifyDb.prepare("SELECT state, settled_micros FROM requests").all() as any[];
229
+ expect(requestRows.length).toBe(5);
230
+ for (const r of requestRows) {
231
+ expect(r.state).toBe("settled");
232
+ expect(r.settled_micros).toBe(110);
233
+ }
234
+
235
+ verifyDb.close();
236
+
237
+ // Step 7: Fire subsequent inference request and assert it HITS local daemon token cache immediately (Zero-Purchase)
238
+ // We capture time elapsed to verify cache speed
239
+ const start = Date.now();
240
+ const hitResponse = await sendProxyRequest("req_subsequent_hit");
241
+ const duration = Date.now() - start;
242
+
243
+ expect(hitResponse.ok).toBe(true);
244
+ const hitData = await hitResponse.json() as any;
245
+ expect(hitData.id).toBe("chatcmpl-e2e-ok");
246
+
247
+ // Total purchases should remain intact, meaning no extra purchases occurred!
248
+ const verifyDb2 = new DatabaseSync(TEMP_SELLER_DB);
249
+ const purchaseCount = (verifyDb2.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
250
+ // 1 original purchase coalesce from PromiseLocks
251
+ expect(purchaseCount).toBe(1);
252
+ verifyDb2.close();
253
+
254
+ // Step 8: Verify newly implemented admin query billing APIs (purchases & requests)
255
+ const billingPurchases = await admin.get("/operator/admin/purchases");
256
+ expect(billingPurchases.purchases.length).toBe(1);
257
+ expect(billingPurchases.purchases[0].state).toBe("funded");
258
+
259
+ const billingRequests = await admin.get("/operator/admin/requests");
260
+ // 5 concurrent requests + 1 subsequent hit = 6 total inference requests tracked on seller database
261
+ expect(billingRequests.requests.length).toBe(6);
262
+ expect(billingRequests.requests.every((r: any) => r.state === "settled")).toBe(true);
263
+ });
264
+ });