@canister-software/consensus-cli 0.1.0-beta.3 → 0.1.0-beta.5
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 +275 -40
- package/assets/dark-logo.png +0 -0
- package/assets/logo-dark.svg +73 -0
- package/assets/logo-light.svg +73 -0
- package/bin/consensus.js +188 -151
- package/dist/index.js +2 -0
- package/dist/proxy-client.js +466 -0
- package/dist/socket-client.js +440 -0
- package/index.d.ts +271 -0
- package/package.json +23 -3
- package/assets/setup.gif +0 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
const DEFAULT_SERVER_URL = process.env.CONSENSUS_SERVER_URL || "https://consensus.canister.software";
|
|
2
|
+
const USD_SCALE = 1_000_000;
|
|
3
|
+
const PRICING_PRESETS = {
|
|
4
|
+
TIME: {
|
|
5
|
+
model: "time",
|
|
6
|
+
pricePerMinute: 0.001,
|
|
7
|
+
pricePerMB: 0,
|
|
8
|
+
},
|
|
9
|
+
DATA: {
|
|
10
|
+
model: "data",
|
|
11
|
+
pricePerMinute: 0,
|
|
12
|
+
pricePerMB: 0.00012,
|
|
13
|
+
},
|
|
14
|
+
HYBRID: {
|
|
15
|
+
model: "hybrid",
|
|
16
|
+
pricePerMinute: 0.0005,
|
|
17
|
+
pricePerMB: 0.0001,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
class SocketClientError extends Error {
|
|
21
|
+
/** HTTP status from token endpoint when available. */
|
|
22
|
+
status;
|
|
23
|
+
/** Parsed server error payload when available. */
|
|
24
|
+
data;
|
|
25
|
+
}
|
|
26
|
+
/** Thrown when requested token cost exceeds remaining websocket budget. */
|
|
27
|
+
class SocketBudgetLimitError extends SocketClientError {
|
|
28
|
+
}
|
|
29
|
+
function trimTrailingSlash(value) {
|
|
30
|
+
return String(value || "").replace(/\/+$/, "");
|
|
31
|
+
}
|
|
32
|
+
function parseMaybeJson(text) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(text);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function parseUsdToMicros(value, fieldName) {
|
|
43
|
+
if (typeof value === "undefined" || value === null)
|
|
44
|
+
return null;
|
|
45
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
46
|
+
throw new TypeError(`${fieldName} must be a non-negative number`);
|
|
47
|
+
}
|
|
48
|
+
const micros = Math.round(value * USD_SCALE);
|
|
49
|
+
const normalized = micros / USD_SCALE;
|
|
50
|
+
if (Math.abs(normalized - value) > 1e-9) {
|
|
51
|
+
throw new TypeError(`${fieldName} supports at most 6 decimal places`);
|
|
52
|
+
}
|
|
53
|
+
return micros;
|
|
54
|
+
}
|
|
55
|
+
function microsToUsd(micros) {
|
|
56
|
+
return Number((micros / USD_SCALE).toFixed(6));
|
|
57
|
+
}
|
|
58
|
+
function calculateSessionCost(pricing, minutes, megabytes) {
|
|
59
|
+
let cost = 0;
|
|
60
|
+
if (pricing.model === "time" || pricing.model === "hybrid") {
|
|
61
|
+
cost += (minutes || 0) * pricing.pricePerMinute;
|
|
62
|
+
}
|
|
63
|
+
if (pricing.model === "data" || pricing.model === "hybrid") {
|
|
64
|
+
cost += (megabytes || 0) * pricing.pricePerMB;
|
|
65
|
+
}
|
|
66
|
+
return cost;
|
|
67
|
+
}
|
|
68
|
+
function normalizeTokenParams(defaults, params) {
|
|
69
|
+
const merged = {
|
|
70
|
+
model: params?.model ?? defaults?.model ?? "hybrid",
|
|
71
|
+
minutes: params?.minutes ?? defaults?.minutes ?? 5,
|
|
72
|
+
megabytes: params?.megabytes ?? defaults?.megabytes ?? 50,
|
|
73
|
+
nodeRegion: params?.nodeRegion ?? defaults?.nodeRegion,
|
|
74
|
+
nodeDomain: params?.nodeDomain ?? defaults?.nodeDomain,
|
|
75
|
+
nodeExclude: params?.nodeExclude ?? defaults?.nodeExclude,
|
|
76
|
+
};
|
|
77
|
+
if (!["hybrid", "time", "data"].includes(merged.model)) {
|
|
78
|
+
throw new SocketClientError(`Invalid model '${String(merged.model)}'`);
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isInteger(merged.minutes) || merged.minutes < 0) {
|
|
81
|
+
throw new SocketClientError("minutes must be a non-negative integer");
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isInteger(merged.megabytes) || merged.megabytes < 0) {
|
|
84
|
+
throw new SocketClientError("megabytes must be a non-negative integer");
|
|
85
|
+
}
|
|
86
|
+
return merged;
|
|
87
|
+
}
|
|
88
|
+
function toTokenHeaders(params) {
|
|
89
|
+
const headers = {};
|
|
90
|
+
if (params?.nodeRegion)
|
|
91
|
+
headers["x-node-region"] = params.nodeRegion;
|
|
92
|
+
if (params?.nodeDomain)
|
|
93
|
+
headers["x-node-domain"] = params.nodeDomain;
|
|
94
|
+
if (params?.nodeExclude)
|
|
95
|
+
headers["x-node-exclude"] = params.nodeExclude;
|
|
96
|
+
return headers;
|
|
97
|
+
}
|
|
98
|
+
async function resolveWebSocketFactory(factory) {
|
|
99
|
+
if (factory)
|
|
100
|
+
return factory;
|
|
101
|
+
if (typeof WebSocket !== "undefined")
|
|
102
|
+
return WebSocket;
|
|
103
|
+
const wsModule = await import("ws");
|
|
104
|
+
const maybeCtor = wsModule.default ||
|
|
105
|
+
wsModule.WebSocket;
|
|
106
|
+
if (typeof maybeCtor !== "function") {
|
|
107
|
+
throw new SocketClientError("Unable to resolve a WebSocket constructor");
|
|
108
|
+
}
|
|
109
|
+
return maybeCtor;
|
|
110
|
+
}
|
|
111
|
+
function addListener(socket, event, handler) {
|
|
112
|
+
if (typeof socket.addEventListener === "function") {
|
|
113
|
+
socket.addEventListener(event, handler);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (typeof socket.on === "function") {
|
|
117
|
+
socket.on(event, handler);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function removeListener(socket, event, handler) {
|
|
121
|
+
if (typeof socket.removeEventListener === "function") {
|
|
122
|
+
socket.removeEventListener(event, handler);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (typeof socket.off === "function") {
|
|
126
|
+
socket.off(event, handler);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (typeof socket.removeListener === "function") {
|
|
130
|
+
socket.removeListener(event, handler);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function getOpenStateValue(socket) {
|
|
134
|
+
const maybeCtor = socket;
|
|
135
|
+
const open = maybeCtor.constructor?.OPEN;
|
|
136
|
+
return typeof open === "number" ? open : 1;
|
|
137
|
+
}
|
|
138
|
+
function getMessagePayload(value) {
|
|
139
|
+
if (value && typeof value === "object" && "data" in value) {
|
|
140
|
+
return value.data;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function toSafeResult(promise) {
|
|
145
|
+
return promise
|
|
146
|
+
.then((data) => ({ ok: true, data }))
|
|
147
|
+
.catch((error) => ({ ok: false, error }));
|
|
148
|
+
}
|
|
149
|
+
export function SocketClient(fetchWithPayment, options = {}) {
|
|
150
|
+
if (typeof fetchWithPayment !== "function") {
|
|
151
|
+
throw new TypeError("SocketClient requires fetchWithPayment as the first argument");
|
|
152
|
+
}
|
|
153
|
+
const baseUrl = trimTrailingSlash(DEFAULT_SERVER_URL);
|
|
154
|
+
const openTimeoutMs = options.openTimeoutMs ?? 12_000;
|
|
155
|
+
const reconnectIntervalMs = options.reconnectIntervalMs ?? 2_000;
|
|
156
|
+
const limitMicros = parseUsdToMicros(options.limit_usd, "limit_usd");
|
|
157
|
+
let lastTokenParams;
|
|
158
|
+
let spentMicros = 0;
|
|
159
|
+
let limitCallbackFired = false;
|
|
160
|
+
let lastQuoteMicros = 0;
|
|
161
|
+
function computeStandDownState(nextCostMicros = 0) {
|
|
162
|
+
if (limitMicros === null)
|
|
163
|
+
return false;
|
|
164
|
+
if (spentMicros >= limitMicros)
|
|
165
|
+
return true;
|
|
166
|
+
return spentMicros + nextCostMicros > limitMicros;
|
|
167
|
+
}
|
|
168
|
+
function getBudget() {
|
|
169
|
+
const remainingMicros = limitMicros === null ? null : Math.max(0, limitMicros - spentMicros);
|
|
170
|
+
return {
|
|
171
|
+
limit_usd: limitMicros === null ? null : microsToUsd(limitMicros),
|
|
172
|
+
spent_usd: microsToUsd(spentMicros),
|
|
173
|
+
remaining_usd: remainingMicros === null ? null : microsToUsd(remainingMicros),
|
|
174
|
+
exhausted: computeStandDownState(),
|
|
175
|
+
last_quote_usd: microsToUsd(lastQuoteMicros),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function isStandDown() {
|
|
179
|
+
const exhausted = computeStandDownState();
|
|
180
|
+
if (exhausted &&
|
|
181
|
+
!limitCallbackFired &&
|
|
182
|
+
typeof options.on_limit_reached === "function") {
|
|
183
|
+
limitCallbackFired = true;
|
|
184
|
+
options.on_limit_reached(getBudget());
|
|
185
|
+
}
|
|
186
|
+
return exhausted;
|
|
187
|
+
}
|
|
188
|
+
function ensureBudgetFor(quotedCostMicros) {
|
|
189
|
+
if (!computeStandDownState(quotedCostMicros))
|
|
190
|
+
return;
|
|
191
|
+
isStandDown();
|
|
192
|
+
throw new SocketBudgetLimitError("WebSocket budget limit reached; token request blocked");
|
|
193
|
+
}
|
|
194
|
+
function incrementSpend(quotedCostMicros) {
|
|
195
|
+
if (quotedCostMicros <= 0)
|
|
196
|
+
return;
|
|
197
|
+
spentMicros += quotedCostMicros;
|
|
198
|
+
if (limitMicros !== null && spentMicros > limitMicros)
|
|
199
|
+
spentMicros = limitMicros;
|
|
200
|
+
isStandDown();
|
|
201
|
+
}
|
|
202
|
+
function resetBudget() {
|
|
203
|
+
spentMicros = 0;
|
|
204
|
+
limitCallbackFired = false;
|
|
205
|
+
lastQuoteMicros = 0;
|
|
206
|
+
}
|
|
207
|
+
function quoteTokenCostMicros(params) {
|
|
208
|
+
const pricingKey = params.model === "time" ? "TIME" : params.model === "data" ? "DATA" : "HYBRID";
|
|
209
|
+
const pricing = PRICING_PRESETS[pricingKey];
|
|
210
|
+
const usd = calculateSessionCost(pricing, params.minutes, params.megabytes);
|
|
211
|
+
return parseUsdToMicros(usd, "session_cost_usd") ?? 0;
|
|
212
|
+
}
|
|
213
|
+
async function requestTokenInternal(params) {
|
|
214
|
+
const normalized = normalizeTokenParams(options.defaults, params);
|
|
215
|
+
lastTokenParams = normalized;
|
|
216
|
+
const quotedCostMicros = quoteTokenCostMicros({
|
|
217
|
+
model: normalized.model,
|
|
218
|
+
minutes: normalized.minutes,
|
|
219
|
+
megabytes: normalized.megabytes,
|
|
220
|
+
});
|
|
221
|
+
lastQuoteMicros = quotedCostMicros;
|
|
222
|
+
ensureBudgetFor(quotedCostMicros);
|
|
223
|
+
const query = new URLSearchParams({
|
|
224
|
+
model: normalized.model,
|
|
225
|
+
minutes: String(normalized.minutes),
|
|
226
|
+
megabytes: String(normalized.megabytes),
|
|
227
|
+
});
|
|
228
|
+
const response = await fetchWithPayment(`${baseUrl}/ws?${query.toString()}`, {
|
|
229
|
+
method: "GET",
|
|
230
|
+
headers: toTokenHeaders(normalized),
|
|
231
|
+
});
|
|
232
|
+
const raw = await response.text();
|
|
233
|
+
const parsed = parseMaybeJson(raw);
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const message = parsed?.message ||
|
|
236
|
+
parsed?.error ||
|
|
237
|
+
`WebSocket token request failed (${response.status})`;
|
|
238
|
+
const error = new SocketClientError(message);
|
|
239
|
+
error.status = response.status;
|
|
240
|
+
error.data = parsed;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const auth = parsed;
|
|
244
|
+
if (!auth?.connect_url || !auth?.token) {
|
|
245
|
+
throw new SocketClientError("Invalid token response: missing token/connect_url");
|
|
246
|
+
}
|
|
247
|
+
incrementSpend(quotedCostMicros);
|
|
248
|
+
return {
|
|
249
|
+
token: String(auth.token),
|
|
250
|
+
connect_url: String(auth.connect_url),
|
|
251
|
+
expires_in: Number(auth.expires_in ?? 0),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async function requestToken(params, safeOptions) {
|
|
255
|
+
const task = requestTokenInternal(params);
|
|
256
|
+
if (safeOptions?.safe)
|
|
257
|
+
return toSafeResult(task);
|
|
258
|
+
return task;
|
|
259
|
+
}
|
|
260
|
+
async function connectInternal(connectUrlOrAuth, callbacks) {
|
|
261
|
+
const initialConnectUrl = typeof connectUrlOrAuth === "string"
|
|
262
|
+
? connectUrlOrAuth
|
|
263
|
+
: connectUrlOrAuth?.connect_url;
|
|
264
|
+
if (!initialConnectUrl) {
|
|
265
|
+
throw new SocketClientError("connect requires connect_url");
|
|
266
|
+
}
|
|
267
|
+
const listeners = {
|
|
268
|
+
open: new Set(),
|
|
269
|
+
message: new Set(),
|
|
270
|
+
close: new Set(),
|
|
271
|
+
error: new Set(),
|
|
272
|
+
};
|
|
273
|
+
const state = {
|
|
274
|
+
connected: false,
|
|
275
|
+
reconnecting: false,
|
|
276
|
+
closedByCaller: false,
|
|
277
|
+
};
|
|
278
|
+
let currentConnectUrl = initialConnectUrl;
|
|
279
|
+
let reconnectTimer = null;
|
|
280
|
+
let activeSocket = null;
|
|
281
|
+
const socketFactory = await resolveWebSocketFactory(options.webSocketFactory);
|
|
282
|
+
const connectHeaders = toTokenHeaders(lastTokenParams ?? options.defaults);
|
|
283
|
+
const emit = (event, ...args) => {
|
|
284
|
+
for (const handler of listeners[event]) {
|
|
285
|
+
handler(...args);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const clearReconnectTimer = () => {
|
|
289
|
+
if (!reconnectTimer)
|
|
290
|
+
return;
|
|
291
|
+
clearTimeout(reconnectTimer);
|
|
292
|
+
reconnectTimer = null;
|
|
293
|
+
};
|
|
294
|
+
const waitForOpen = (socket) => new Promise((resolve, reject) => {
|
|
295
|
+
let timeout = null;
|
|
296
|
+
const cleanup = () => {
|
|
297
|
+
if (timeout)
|
|
298
|
+
clearTimeout(timeout);
|
|
299
|
+
removeListener(socket, "open", onOpen);
|
|
300
|
+
removeListener(socket, "error", onError);
|
|
301
|
+
removeListener(socket, "close", onClose);
|
|
302
|
+
};
|
|
303
|
+
const onOpen = () => {
|
|
304
|
+
cleanup();
|
|
305
|
+
resolve();
|
|
306
|
+
};
|
|
307
|
+
const onError = (error) => {
|
|
308
|
+
cleanup();
|
|
309
|
+
reject(error);
|
|
310
|
+
};
|
|
311
|
+
const onClose = () => {
|
|
312
|
+
cleanup();
|
|
313
|
+
reject(new SocketClientError("Socket closed before opening"));
|
|
314
|
+
};
|
|
315
|
+
addListener(socket, "open", onOpen);
|
|
316
|
+
addListener(socket, "error", onError);
|
|
317
|
+
addListener(socket, "close", onClose);
|
|
318
|
+
timeout = setTimeout(() => {
|
|
319
|
+
cleanup();
|
|
320
|
+
reject(new SocketClientError("Socket open timeout"));
|
|
321
|
+
}, openTimeoutMs);
|
|
322
|
+
});
|
|
323
|
+
const openSocket = async () => {
|
|
324
|
+
let socketInstance;
|
|
325
|
+
try {
|
|
326
|
+
socketInstance = new socketFactory(currentConnectUrl, { headers: connectHeaders });
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
socketInstance = new socketFactory(currentConnectUrl);
|
|
330
|
+
}
|
|
331
|
+
addListener(socketInstance, "open", () => {
|
|
332
|
+
state.connected = true;
|
|
333
|
+
state.reconnecting = false;
|
|
334
|
+
callbacks?.onOpen?.();
|
|
335
|
+
emit("open");
|
|
336
|
+
});
|
|
337
|
+
addListener(socketInstance, "message", (event) => {
|
|
338
|
+
const payload = getMessagePayload(event);
|
|
339
|
+
callbacks?.onMessage?.(payload);
|
|
340
|
+
emit("message", payload);
|
|
341
|
+
});
|
|
342
|
+
addListener(socketInstance, "error", (error) => {
|
|
343
|
+
callbacks?.onError?.(error);
|
|
344
|
+
emit("error", error);
|
|
345
|
+
});
|
|
346
|
+
addListener(socketInstance, "close", (event) => {
|
|
347
|
+
state.connected = false;
|
|
348
|
+
callbacks?.onClose?.(event);
|
|
349
|
+
emit("close", event);
|
|
350
|
+
if (state.closedByCaller)
|
|
351
|
+
return;
|
|
352
|
+
clearReconnectTimer();
|
|
353
|
+
reconnectTimer = setTimeout(async () => {
|
|
354
|
+
if (state.closedByCaller)
|
|
355
|
+
return;
|
|
356
|
+
state.reconnecting = true;
|
|
357
|
+
try {
|
|
358
|
+
if (lastTokenParams) {
|
|
359
|
+
const auth = await requestTokenInternal(lastTokenParams);
|
|
360
|
+
currentConnectUrl = auth.connect_url;
|
|
361
|
+
}
|
|
362
|
+
await openSocket();
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
callbacks?.onError?.(error);
|
|
366
|
+
emit("error", error);
|
|
367
|
+
if (error instanceof SocketBudgetLimitError) {
|
|
368
|
+
state.reconnecting = false;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!state.closedByCaller) {
|
|
372
|
+
reconnectTimer = setTimeout(async () => {
|
|
373
|
+
if (!state.closedByCaller) {
|
|
374
|
+
state.reconnecting = true;
|
|
375
|
+
try {
|
|
376
|
+
if (lastTokenParams) {
|
|
377
|
+
const auth = await requestTokenInternal(lastTokenParams);
|
|
378
|
+
currentConnectUrl = auth.connect_url;
|
|
379
|
+
}
|
|
380
|
+
await openSocket();
|
|
381
|
+
}
|
|
382
|
+
catch (retryError) {
|
|
383
|
+
callbacks?.onError?.(retryError);
|
|
384
|
+
emit("error", retryError);
|
|
385
|
+
if (retryError instanceof SocketBudgetLimitError) {
|
|
386
|
+
state.reconnecting = false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}, reconnectIntervalMs);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}, reconnectIntervalMs);
|
|
394
|
+
});
|
|
395
|
+
activeSocket = socketInstance;
|
|
396
|
+
await waitForOpen(socketInstance);
|
|
397
|
+
};
|
|
398
|
+
await openSocket();
|
|
399
|
+
return {
|
|
400
|
+
send(data) {
|
|
401
|
+
if (!activeSocket) {
|
|
402
|
+
throw new SocketClientError("No active socket session");
|
|
403
|
+
}
|
|
404
|
+
const openState = getOpenStateValue(activeSocket);
|
|
405
|
+
if (activeSocket.readyState !== openState) {
|
|
406
|
+
throw new SocketClientError("Socket is not open");
|
|
407
|
+
}
|
|
408
|
+
activeSocket.send(data);
|
|
409
|
+
},
|
|
410
|
+
close(code, reason) {
|
|
411
|
+
state.closedByCaller = true;
|
|
412
|
+
state.reconnecting = false;
|
|
413
|
+
clearReconnectTimer();
|
|
414
|
+
activeSocket?.close(code, reason);
|
|
415
|
+
},
|
|
416
|
+
on(event, handler) {
|
|
417
|
+
listeners[event].add(handler);
|
|
418
|
+
},
|
|
419
|
+
off(event, handler) {
|
|
420
|
+
listeners[event].delete(handler);
|
|
421
|
+
},
|
|
422
|
+
getState() {
|
|
423
|
+
return { ...state };
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function connect(connectUrlOrAuth, callbacks, safeOptions) {
|
|
428
|
+
const task = connectInternal(connectUrlOrAuth, callbacks);
|
|
429
|
+
if (safeOptions?.safe)
|
|
430
|
+
return toSafeResult(task);
|
|
431
|
+
return task;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
requestToken,
|
|
435
|
+
connect,
|
|
436
|
+
getBudget,
|
|
437
|
+
resetBudget,
|
|
438
|
+
isStandDown,
|
|
439
|
+
};
|
|
440
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
export interface ProxyClientOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Route filtering behavior for inbound server paths.
|
|
4
|
+
* - "inclusive": proxy everything except `routes`
|
|
5
|
+
* - "exclusive": proxy only `routes`
|
|
6
|
+
*/
|
|
7
|
+
mode?: "inclusive" | "exclusive";
|
|
8
|
+
/**
|
|
9
|
+
* Path rules used with `mode`, for example `["/health", "/metrics"]`.
|
|
10
|
+
* Query params are ignored; matching is based on path only.
|
|
11
|
+
*/
|
|
12
|
+
routes?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Path matcher behavior for `routes`.
|
|
15
|
+
* - false (default): exact path only (`/route` does not match `/route/subroute`)
|
|
16
|
+
* - true: include subroutes (`/route` matches `/route/*`)
|
|
17
|
+
*/
|
|
18
|
+
matchSubroutes?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Interception strategy.
|
|
21
|
+
* - "auto": globally intercepts `fetch` for route-matched request scope
|
|
22
|
+
* - "manual": does not intercept global `fetch`; use `req.consensus.fetch` / `request`
|
|
23
|
+
*/
|
|
24
|
+
strategy?: "auto" | "manual";
|
|
25
|
+
/**
|
|
26
|
+
* Cache time-to-live in seconds for proxy responses.
|
|
27
|
+
* Sent as `x-cache-ttl`; controls how long deduped responses can be reused.
|
|
28
|
+
*/
|
|
29
|
+
cache_ttl?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Enables verbose proxy response payload.
|
|
32
|
+
* When true, proxy responses include `meta` with fields like:
|
|
33
|
+
* `cached`, `dedupe_key`, `processing_ms`, and `timestamp`.
|
|
34
|
+
*/
|
|
35
|
+
verbose?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Preferred proxy region, for example `"us-east"`.
|
|
38
|
+
* Sent as `x-node-region`.
|
|
39
|
+
*/
|
|
40
|
+
node_region?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Force routing through a specific node domain, for example:
|
|
43
|
+
* `"nodexyz.consensus.canister.software"`.
|
|
44
|
+
* Sent as `x-node-domain`.
|
|
45
|
+
*/
|
|
46
|
+
node_domain?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Exclude a specific node/domain from routing.
|
|
49
|
+
* Sent as `x-node-exclude`.
|
|
50
|
+
*/
|
|
51
|
+
node_exclude?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Max proxy spend in USD (up to 6 decimals).
|
|
54
|
+
* Once exhausted, ProxyClient stands down and uses direct fetch.
|
|
55
|
+
*/
|
|
56
|
+
limit_usd?: number;
|
|
57
|
+
/** Called once when proxy budget is exhausted and stand-down activates. */
|
|
58
|
+
on_limit_reached?: (budget: ProxyBudgetSnapshot) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProxyBudgetSnapshot {
|
|
62
|
+
/** Configured max spend in USD, or null when no limit is configured. */
|
|
63
|
+
limit_usd: number | null;
|
|
64
|
+
/** Fixed server proxy charge per paid request. */
|
|
65
|
+
request_cost_usd: number;
|
|
66
|
+
/** Total spent so far in USD. */
|
|
67
|
+
spent_usd: number;
|
|
68
|
+
/** Remaining budget in USD, or null when unlimited. */
|
|
69
|
+
remaining_usd: number | null;
|
|
70
|
+
/** True when budget guard has disabled further proxying. */
|
|
71
|
+
exhausted: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProxyResponseShape {
|
|
75
|
+
/** HTTP status code returned by proxy response. */
|
|
76
|
+
status: number;
|
|
77
|
+
/** HTTP reason phrase from proxy response. */
|
|
78
|
+
statusText: string;
|
|
79
|
+
/** Response headers from the target/proxy response. */
|
|
80
|
+
headers: Record<string, string>;
|
|
81
|
+
/** Parsed payload body (JSON/object/string) returned by proxy. */
|
|
82
|
+
data: unknown;
|
|
83
|
+
/**
|
|
84
|
+
* Verbose metadata returned when `verbose` is enabled.
|
|
85
|
+
* Common fields:
|
|
86
|
+
* - `cached`: whether response came from proxy cache
|
|
87
|
+
* - `dedupe_key`: deduplication key used by proxy
|
|
88
|
+
* - `processing_ms`: end-to-end proxy processing duration
|
|
89
|
+
* - `timestamp`: ISO timestamp generated by proxy
|
|
90
|
+
*/
|
|
91
|
+
meta: {
|
|
92
|
+
cached?: boolean;
|
|
93
|
+
dedupe_key?: string;
|
|
94
|
+
processing_ms?: number;
|
|
95
|
+
timestamp?: string;
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
} | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ProxyClientRequestContext {
|
|
101
|
+
consensus?: {
|
|
102
|
+
strategy?: "auto" | "manual";
|
|
103
|
+
shouldProxy?: boolean;
|
|
104
|
+
fetch?: (
|
|
105
|
+
input: RequestInfo | URL,
|
|
106
|
+
init?: RequestInit,
|
|
107
|
+
perRequestOptions?: Partial<ProxyClientOptions>
|
|
108
|
+
) => Promise<Response>;
|
|
109
|
+
request?: (
|
|
110
|
+
payload: {
|
|
111
|
+
target_url?: string;
|
|
112
|
+
method?: string;
|
|
113
|
+
headers?: Record<string, string>;
|
|
114
|
+
body?: unknown;
|
|
115
|
+
},
|
|
116
|
+
perRequestOptions?: Partial<ProxyClientOptions>
|
|
117
|
+
) => Promise<ProxyResponseShape>;
|
|
118
|
+
passthroughFetch?: ((input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) | null;
|
|
119
|
+
createFetch?: (pathname?: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
120
|
+
getBudget?: () => ProxyBudgetSnapshot;
|
|
121
|
+
isStandDown?: () => boolean;
|
|
122
|
+
};
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ProxyClientRuntime {
|
|
127
|
+
fetch(
|
|
128
|
+
input: RequestInfo | URL,
|
|
129
|
+
init?: RequestInit,
|
|
130
|
+
perRequestOptions?: Partial<ProxyClientOptions>
|
|
131
|
+
): Promise<Response>;
|
|
132
|
+
request(
|
|
133
|
+
payload: {
|
|
134
|
+
target_url?: string;
|
|
135
|
+
method?: string;
|
|
136
|
+
headers?: Record<string, string>;
|
|
137
|
+
body?: unknown;
|
|
138
|
+
},
|
|
139
|
+
perRequestOptions?: Partial<ProxyClientOptions>
|
|
140
|
+
): Promise<ProxyResponseShape>;
|
|
141
|
+
runWithPath<T>(pathname: string, run: () => T | Promise<T>): Promise<T>;
|
|
142
|
+
createFetch(pathname?: string): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
143
|
+
getBudget(): ProxyBudgetSnapshot;
|
|
144
|
+
resetBudget(): void;
|
|
145
|
+
isStandDown(): boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type ProxyClientMiddleware = ((
|
|
149
|
+
req: ProxyClientRequestContext,
|
|
150
|
+
res: unknown,
|
|
151
|
+
next: (err?: unknown) => void
|
|
152
|
+
) => void) &
|
|
153
|
+
ProxyClientRuntime;
|
|
154
|
+
|
|
155
|
+
export declare function ProxyClient(
|
|
156
|
+
fetchWithPayment: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
|
|
157
|
+
options?: ProxyClientOptions
|
|
158
|
+
): ProxyClientMiddleware;
|
|
159
|
+
|
|
160
|
+
export type ConsensusSocketModel = "hybrid" | "time" | "data";
|
|
161
|
+
|
|
162
|
+
export interface ConsensusSocketTokenParams {
|
|
163
|
+
/** Billing model for session pricing. */
|
|
164
|
+
model?: ConsensusSocketModel;
|
|
165
|
+
/** Session duration to purchase (integer minutes, >= 0). */
|
|
166
|
+
minutes?: number;
|
|
167
|
+
/** Session data amount to purchase (integer MB, >= 0). */
|
|
168
|
+
megabytes?: number;
|
|
169
|
+
/** Optional preferred node region for token/session routing (e.g. `"us-east"`). */
|
|
170
|
+
nodeRegion?: string;
|
|
171
|
+
/** Optional hard route to a specific node domain (e.g. `"nodexyz.consensus.canister.software"`). */
|
|
172
|
+
nodeDomain?: string;
|
|
173
|
+
/** Optional node/domain to exclude from routing. */
|
|
174
|
+
nodeExclude?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ConsensusSocketTokenAuth {
|
|
178
|
+
token: string;
|
|
179
|
+
connect_url: string;
|
|
180
|
+
expires_in: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface ConsensusSocketConnectTarget {
|
|
184
|
+
connect_url: string;
|
|
185
|
+
token?: string;
|
|
186
|
+
expires_in?: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ConsensusSocketCallbacks {
|
|
190
|
+
onOpen?: () => void;
|
|
191
|
+
onMessage?: (data: unknown) => void;
|
|
192
|
+
onClose?: (event?: unknown) => void;
|
|
193
|
+
onError?: (error: unknown) => void;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface ConsensusSocketSafeResult<T> {
|
|
197
|
+
ok: boolean;
|
|
198
|
+
data?: T;
|
|
199
|
+
error?: unknown;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface ConsensusSocketSessionState {
|
|
203
|
+
connected: boolean;
|
|
204
|
+
reconnecting: boolean;
|
|
205
|
+
closedByCaller: boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ConsensusSocketSession {
|
|
209
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
210
|
+
close(code?: number, reason?: string): void;
|
|
211
|
+
on(event: "open" | "message" | "close" | "error", handler: (...args: unknown[]) => void): void;
|
|
212
|
+
off(event: "open" | "message" | "close" | "error", handler: (...args: unknown[]) => void): void;
|
|
213
|
+
getState(): ConsensusSocketSessionState;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface ConsensusSocketClientOptions {
|
|
217
|
+
/** Custom WebSocket constructor; auto-detected when omitted. */
|
|
218
|
+
webSocketFactory?: new (...args: unknown[]) => unknown;
|
|
219
|
+
/** Max time to wait for websocket open before failing (ms). */
|
|
220
|
+
openTimeoutMs?: number;
|
|
221
|
+
/** Fixed reconnect delay (ms). */
|
|
222
|
+
reconnectIntervalMs?: number;
|
|
223
|
+
/** Default token params merged into each requestToken call. */
|
|
224
|
+
defaults?: ConsensusSocketTokenParams;
|
|
225
|
+
/** Max websocket spend in USD (up to 6 decimals) before token requests are blocked. */
|
|
226
|
+
limit_usd?: number;
|
|
227
|
+
/** Called once when websocket budget is exhausted. */
|
|
228
|
+
on_limit_reached?: (budget: ConsensusSocketBudgetSnapshot) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ConsensusSocketBudgetSnapshot {
|
|
232
|
+
/** Configured max spend in USD, or null when no limit is configured. */
|
|
233
|
+
limit_usd: number | null;
|
|
234
|
+
/** Total spent so far in USD. */
|
|
235
|
+
spent_usd: number;
|
|
236
|
+
/** Remaining budget in USD, or null when unlimited. */
|
|
237
|
+
remaining_usd: number | null;
|
|
238
|
+
/** True when budget guard blocks further token purchases. */
|
|
239
|
+
exhausted: boolean;
|
|
240
|
+
/** Last locally quoted token/session price in USD. */
|
|
241
|
+
last_quote_usd: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface ConsensusSocketClient {
|
|
245
|
+
requestToken(
|
|
246
|
+
params?: ConsensusSocketTokenParams,
|
|
247
|
+
options?: { safe?: false }
|
|
248
|
+
): Promise<ConsensusSocketTokenAuth>;
|
|
249
|
+
requestToken(
|
|
250
|
+
params: ConsensusSocketTokenParams | undefined,
|
|
251
|
+
options: { safe: true }
|
|
252
|
+
): Promise<ConsensusSocketSafeResult<ConsensusSocketTokenAuth>>;
|
|
253
|
+
connect(
|
|
254
|
+
connectUrlOrAuth: string | ConsensusSocketConnectTarget,
|
|
255
|
+
callbacks?: ConsensusSocketCallbacks,
|
|
256
|
+
options?: { safe?: false }
|
|
257
|
+
): Promise<ConsensusSocketSession>;
|
|
258
|
+
connect(
|
|
259
|
+
connectUrlOrAuth: string | ConsensusSocketConnectTarget,
|
|
260
|
+
callbacks: ConsensusSocketCallbacks | undefined,
|
|
261
|
+
options: { safe: true }
|
|
262
|
+
): Promise<ConsensusSocketSafeResult<ConsensusSocketSession>>;
|
|
263
|
+
getBudget(): ConsensusSocketBudgetSnapshot;
|
|
264
|
+
resetBudget(): void;
|
|
265
|
+
isStandDown(): boolean;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export declare function SocketClient(
|
|
269
|
+
fetchWithPayment: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
|
|
270
|
+
options?: ConsensusSocketClientOptions
|
|
271
|
+
): ConsensusSocketClient;
|