edge-book 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edge-book.js +2965 -0
- package/package.json +13 -5
- package/bin/edge-book.js +0 -7
- package/src/cli.ts +0 -301
- package/src/dialout.ts +0 -453
- package/src/edge-book.ts +0 -1371
- package/src/http.ts +0 -1481
package/src/dialout.ts
DELETED
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import http from "node:http";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { EdgeBookError, EdgeBookStore } from "./edge-book.ts";
|
|
6
|
-
import { startEdgeBookServer } from "./http.ts";
|
|
7
|
-
|
|
8
|
-
const KEY_FILE = "host-dialout-key.json";
|
|
9
|
-
const DEFAULT_PAIR_TTL_MS = 5 * 60 * 1000;
|
|
10
|
-
const DEFAULT_HEARTBEAT_MS = 25_000;
|
|
11
|
-
const DEFAULT_BACKOFF_MS = 1_000;
|
|
12
|
-
const MAX_BACKOFF_MS = 30_000;
|
|
13
|
-
const PAIRING_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
14
|
-
|
|
15
|
-
export interface DialoutKey {
|
|
16
|
-
schema: "edge-book-host-dialout-key/0.1";
|
|
17
|
-
key_id: string;
|
|
18
|
-
agent_key: string;
|
|
19
|
-
public_key_pem: string;
|
|
20
|
-
private_key_pem: string;
|
|
21
|
-
created_at: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface DialoutApiRequest {
|
|
25
|
-
type: "host.api.request" | "api_request";
|
|
26
|
-
id?: string;
|
|
27
|
-
request_id?: string;
|
|
28
|
-
method?: string;
|
|
29
|
-
path: string;
|
|
30
|
-
query?: string;
|
|
31
|
-
headers?: Record<string, string>;
|
|
32
|
-
body?: unknown;
|
|
33
|
-
body_b64?: string | null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface DialoutApiResponse {
|
|
37
|
-
type: "api_response";
|
|
38
|
-
id: string;
|
|
39
|
-
request_id: string;
|
|
40
|
-
status: number;
|
|
41
|
-
headers: Record<string, string>;
|
|
42
|
-
body_b64: string;
|
|
43
|
-
body?: unknown;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface PairRegistration {
|
|
47
|
-
code: string;
|
|
48
|
-
frame: {
|
|
49
|
-
type: "pair_register";
|
|
50
|
-
code: string;
|
|
51
|
-
ttl_ms: number;
|
|
52
|
-
request_id: string;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface SessionsRevokeFrame {
|
|
57
|
-
type: "sessions_revoke";
|
|
58
|
-
request_id: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface SessionsRevokeAck {
|
|
62
|
-
type: "sessions_revoke_ok";
|
|
63
|
-
request_id?: string;
|
|
64
|
-
channel_id?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface DialoutSocket {
|
|
68
|
-
readyState?: number;
|
|
69
|
-
send(data: string): void;
|
|
70
|
-
close(): void;
|
|
71
|
-
addEventListener?(event: "open" | "message" | "close" | "error", handler: (event?: unknown) => void): void;
|
|
72
|
-
onopen?: (event?: unknown) => void;
|
|
73
|
-
onmessage?: (event: { data: unknown }) => void;
|
|
74
|
-
onclose?: (event?: unknown) => void;
|
|
75
|
-
onerror?: (event?: unknown) => void;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface DialoutClientOptions {
|
|
79
|
-
home?: string;
|
|
80
|
-
host: string;
|
|
81
|
-
heartbeatMs?: number;
|
|
82
|
-
reconnect?: boolean;
|
|
83
|
-
backoffMs?: number;
|
|
84
|
-
socketFactory?: (url: string) => DialoutSocket;
|
|
85
|
-
openLocalApi?: boolean;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface LocalApi {
|
|
89
|
-
server: http.Server;
|
|
90
|
-
baseUrl: string;
|
|
91
|
-
sessionId: string;
|
|
92
|
-
csrf: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function now(): string {
|
|
96
|
-
return new Date().toISOString();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function keyId(agentKey: string): string {
|
|
100
|
-
return `agent_${crypto.createHash("sha256").update(agentKey).digest("base64url").slice(0, 32)}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function channelIdForKey(key: DialoutKey): string {
|
|
104
|
-
return crypto.createHash("sha256").update(key.agent_key).digest("hex");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function chmodBestEffort(file: string, mode: number): Promise<void> {
|
|
108
|
-
if (process.platform === "win32") return;
|
|
109
|
-
try {
|
|
110
|
-
await fs.chmod(file, mode);
|
|
111
|
-
} catch {
|
|
112
|
-
// Some mounted filesystems do not honor POSIX modes.
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function loadOrCreateDialoutKey(store: EdgeBookStore): Promise<DialoutKey> {
|
|
117
|
-
const file = store.file(KEY_FILE);
|
|
118
|
-
try {
|
|
119
|
-
const existing = JSON.parse(await fs.readFile(file, "utf8")) as Partial<DialoutKey>;
|
|
120
|
-
if (existing.agent_key && existing.public_key_pem && existing.private_key_pem && existing.key_id) return existing as DialoutKey;
|
|
121
|
-
if (existing.public_key_pem && existing.private_key_pem && existing.key_id) {
|
|
122
|
-
const migrated = {
|
|
123
|
-
...existing,
|
|
124
|
-
agent_key: `ed25519:${Buffer.from(existing.public_key_pem, "utf8").toString("base64")}`
|
|
125
|
-
} as DialoutKey;
|
|
126
|
-
migrated.key_id = keyId(migrated.agent_key);
|
|
127
|
-
await fs.writeFile(file, `${JSON.stringify(migrated, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
128
|
-
await chmodBestEffort(file, 0o600);
|
|
129
|
-
return migrated;
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const pair = crypto.generateKeyPairSync("ed25519");
|
|
136
|
-
const publicKeyPem = pair.publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
137
|
-
const privateKeyPem = pair.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
138
|
-
const publicKeyDer = pair.publicKey.export({ type: "spki", format: "der" });
|
|
139
|
-
const agentKey = `ed25519:${Buffer.from(publicKeyDer).toString("base64")}`;
|
|
140
|
-
const key: DialoutKey = {
|
|
141
|
-
schema: "edge-book-host-dialout-key/0.1",
|
|
142
|
-
key_id: keyId(agentKey),
|
|
143
|
-
agent_key: agentKey,
|
|
144
|
-
public_key_pem: publicKeyPem,
|
|
145
|
-
private_key_pem: privateKeyPem,
|
|
146
|
-
created_at: now()
|
|
147
|
-
};
|
|
148
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
149
|
-
await fs.writeFile(file, `${JSON.stringify(key, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
150
|
-
await chmodBestEffort(file, 0o600);
|
|
151
|
-
return key;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function generatePairingCode(length = 8): string {
|
|
155
|
-
let code = "";
|
|
156
|
-
for (let i = 0; i < length; i += 1) {
|
|
157
|
-
code += PAIRING_ALPHABET[crypto.randomInt(PAIRING_ALPHABET.length)];
|
|
158
|
-
}
|
|
159
|
-
return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function createPairRegistration(store: EdgeBookStore, ttlMs = DEFAULT_PAIR_TTL_MS): Promise<PairRegistration> {
|
|
163
|
-
const code = generatePairingCode();
|
|
164
|
-
return {
|
|
165
|
-
code,
|
|
166
|
-
frame: {
|
|
167
|
-
type: "pair_register",
|
|
168
|
-
code,
|
|
169
|
-
ttl_ms: ttlMs,
|
|
170
|
-
request_id: crypto.randomUUID()
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function createSessionsRevokeFrame(store: EdgeBookStore): Promise<SessionsRevokeFrame> {
|
|
176
|
-
await loadOrCreateDialoutKey(store);
|
|
177
|
-
return {
|
|
178
|
-
type: "sessions_revoke",
|
|
179
|
-
request_id: crypto.randomUUID()
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function socketFactory(url: string): DialoutSocket {
|
|
184
|
-
const SocketCtor = globalThis.WebSocket;
|
|
185
|
-
if (!SocketCtor) throw new EdgeBookError("websocket_unavailable", "This Node runtime does not provide global WebSocket");
|
|
186
|
-
return new SocketCtor(url) as unknown as DialoutSocket;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function addSocketListener(socket: DialoutSocket, event: "open" | "message" | "close" | "error", handler: (event?: unknown) => void): void {
|
|
190
|
-
if (socket.addEventListener) {
|
|
191
|
-
socket.addEventListener(event, handler);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const prop = `on${event}` as "onopen" | "onmessage" | "onclose" | "onerror";
|
|
195
|
-
socket[prop] = handler as never;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function serverBaseUrl(server: http.Server): string {
|
|
199
|
-
const address = server.address();
|
|
200
|
-
if (!address || typeof address === "string") throw new EdgeBookError("local_api_unavailable", "Local API server did not expose a port");
|
|
201
|
-
return `http://127.0.0.1:${address.port}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function closeServer(server: http.Server): Promise<void> {
|
|
205
|
-
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function openLocalApi(store: EdgeBookStore): Promise<LocalApi> {
|
|
209
|
-
const server = await startEdgeBookServer({ home: store.home, host: "127.0.0.1", port: 0 });
|
|
210
|
-
const baseUrl = serverBaseUrl(server);
|
|
211
|
-
const login = await fetch(`${baseUrl}/auth/login`, {
|
|
212
|
-
method: "POST",
|
|
213
|
-
headers: { "content-type": "application/json" },
|
|
214
|
-
body: JSON.stringify({ auth_method: "future-remote-auth", ttl_ms: 24 * 60 * 60 * 1000 })
|
|
215
|
-
});
|
|
216
|
-
if (!login.ok) {
|
|
217
|
-
await closeServer(server);
|
|
218
|
-
throw new EdgeBookError("local_api_login_failed", `Local API login failed: ${login.status}`);
|
|
219
|
-
}
|
|
220
|
-
const body = await login.json() as { session_id: string; csrf_token: string };
|
|
221
|
-
return { server, baseUrl, sessionId: body.session_id, csrf: body.csrf_token };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function normalizeApiPath(value: string): string {
|
|
225
|
-
if (!value.startsWith("/api/")) throw new EdgeBookError("invalid_proxy_path", "Dial-out only proxies /api/* JSON requests");
|
|
226
|
-
return value;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function apiUrl(baseUrl: string, frame: DialoutApiRequest): string {
|
|
230
|
-
return `${baseUrl}${normalizeApiPath(frame.path)}${frame.query || ""}`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function requestBody(frame: DialoutApiRequest, method: string): Buffer | undefined {
|
|
234
|
-
if (method === "GET" || method === "HEAD") return undefined;
|
|
235
|
-
if (typeof frame.body_b64 === "string") return Buffer.from(frame.body_b64, "base64");
|
|
236
|
-
return Buffer.from(JSON.stringify(frame.body ?? {}), "utf8");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export class EdgeBookDialoutClient {
|
|
240
|
-
private options: Required<Omit<DialoutClientOptions, "home">> & { home?: string };
|
|
241
|
-
private store: EdgeBookStore;
|
|
242
|
-
private socket?: DialoutSocket;
|
|
243
|
-
private localApi?: LocalApi;
|
|
244
|
-
private heartbeat?: ReturnType<typeof setInterval>;
|
|
245
|
-
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
|
246
|
-
private stopped = false;
|
|
247
|
-
private currentBackoff: number;
|
|
248
|
-
private opened?: { resolve: () => void; reject: (error: Error) => void };
|
|
249
|
-
private pendingSessionRevokes = new Map<string, {
|
|
250
|
-
resolve: (ack: SessionsRevokeAck) => void;
|
|
251
|
-
reject: (error: Error) => void;
|
|
252
|
-
timer: ReturnType<typeof setTimeout>;
|
|
253
|
-
}>();
|
|
254
|
-
|
|
255
|
-
constructor(options: DialoutClientOptions) {
|
|
256
|
-
this.options = {
|
|
257
|
-
heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
|
|
258
|
-
reconnect: options.reconnect ?? true,
|
|
259
|
-
backoffMs: options.backoffMs ?? DEFAULT_BACKOFF_MS,
|
|
260
|
-
socketFactory: options.socketFactory ?? socketFactory,
|
|
261
|
-
openLocalApi: options.openLocalApi ?? true,
|
|
262
|
-
host: options.host,
|
|
263
|
-
home: options.home
|
|
264
|
-
};
|
|
265
|
-
this.store = new EdgeBookStore({ home: options.home });
|
|
266
|
-
this.currentBackoff = this.options.backoffMs;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async start(): Promise<void> {
|
|
270
|
-
this.stopped = false;
|
|
271
|
-
await this.connect();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async stop(): Promise<void> {
|
|
275
|
-
this.stopped = true;
|
|
276
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
277
|
-
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
278
|
-
this.socket?.close();
|
|
279
|
-
if (this.localApi) await closeServer(this.localApi.server);
|
|
280
|
-
this.localApi = undefined;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async pair(ttlMs = DEFAULT_PAIR_TTL_MS): Promise<PairRegistration> {
|
|
284
|
-
const registration = await createPairRegistration(this.store, ttlMs);
|
|
285
|
-
this.send(registration.frame);
|
|
286
|
-
return registration;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async revokeSessions(): Promise<SessionsRevokeFrame> {
|
|
290
|
-
const frame = await createSessionsRevokeFrame(this.store);
|
|
291
|
-
this.send(frame);
|
|
292
|
-
return frame;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async revokeSessionsAndWait(timeoutMs = 5_000): Promise<{ frame: SessionsRevokeFrame; ack: SessionsRevokeAck }> {
|
|
296
|
-
const frame = await createSessionsRevokeFrame(this.store);
|
|
297
|
-
const ackPromise = new Promise<SessionsRevokeAck>((resolve, reject) => {
|
|
298
|
-
const timer = setTimeout(() => {
|
|
299
|
-
this.pendingSessionRevokes.delete(frame.request_id);
|
|
300
|
-
reject(new EdgeBookError("host_revoke_timeout", "Timed out waiting for sessions_revoke_ok"));
|
|
301
|
-
}, timeoutMs);
|
|
302
|
-
this.pendingSessionRevokes.set(frame.request_id, { resolve, reject, timer });
|
|
303
|
-
});
|
|
304
|
-
this.send(frame);
|
|
305
|
-
return { frame, ack: await ackPromise };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
private async connect(): Promise<void> {
|
|
309
|
-
if (this.options.openLocalApi && !this.localApi) this.localApi = await openLocalApi(this.store);
|
|
310
|
-
const socket = this.options.socketFactory(this.options.host);
|
|
311
|
-
this.socket = socket;
|
|
312
|
-
|
|
313
|
-
const opened = new Promise<void>((resolve, reject) => {
|
|
314
|
-
this.opened = { resolve, reject };
|
|
315
|
-
addSocketListener(socket, "open", async () => {
|
|
316
|
-
try {
|
|
317
|
-
this.currentBackoff = this.options.backoffMs;
|
|
318
|
-
const key = await loadOrCreateDialoutKey(this.store);
|
|
319
|
-
const identity = await this.store.identity();
|
|
320
|
-
this.send({
|
|
321
|
-
type: "hello",
|
|
322
|
-
agent_key: key.agent_key,
|
|
323
|
-
agent_did: identity.agent_id,
|
|
324
|
-
version: "0.1.0",
|
|
325
|
-
nonce: crypto.randomUUID()
|
|
326
|
-
});
|
|
327
|
-
} catch (error) {
|
|
328
|
-
this.opened = undefined;
|
|
329
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
addSocketListener(socket, "message", (event) => {
|
|
335
|
-
void this.handleMessage((event as { data: unknown })?.data);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
addSocketListener(socket, "close", () => {
|
|
339
|
-
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
340
|
-
if (!this.stopped && this.options.reconnect) this.scheduleReconnect();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
await opened;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private scheduleReconnect(): void {
|
|
347
|
-
const delay = this.currentBackoff;
|
|
348
|
-
this.currentBackoff = Math.min(MAX_BACKOFF_MS, Math.round(this.currentBackoff * 1.7));
|
|
349
|
-
this.reconnectTimer = setTimeout(() => {
|
|
350
|
-
void this.connect();
|
|
351
|
-
}, delay);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private send(value: unknown): void {
|
|
355
|
-
this.socket?.send(JSON.stringify(value));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private async handleMessage(data: unknown): Promise<void> {
|
|
359
|
-
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
360
|
-
const frame = JSON.parse(text) as DialoutApiRequest;
|
|
361
|
-
if ((frame as { type?: string }).type === "hello_ok") {
|
|
362
|
-
this.opened?.resolve();
|
|
363
|
-
this.opened = undefined;
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if ((frame as { type?: string; error?: string }).type === "hello_err") {
|
|
367
|
-
const error = new EdgeBookError("host_hello_failed", (frame as { error?: string }).error || "Host rejected hello");
|
|
368
|
-
this.opened?.reject(error);
|
|
369
|
-
this.opened = undefined;
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if ((frame as { type?: string }).type === "ping") {
|
|
373
|
-
this.send({ type: "pong" });
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if ((frame as { type?: string }).type === "pair_register_ok" || (frame as { type?: string }).type === "pair_register_err") return;
|
|
377
|
-
if ((frame as { type?: string }).type === "sessions_revoke_ok") {
|
|
378
|
-
const ack = frame as unknown as SessionsRevokeAck;
|
|
379
|
-
const pending = this.pendingSessionRevokes.get(ack.request_id || "");
|
|
380
|
-
if (pending) {
|
|
381
|
-
clearTimeout(pending.timer);
|
|
382
|
-
this.pendingSessionRevokes.delete(ack.request_id || "");
|
|
383
|
-
pending.resolve(ack);
|
|
384
|
-
}
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if ((frame as { type?: string }).type === "error") return;
|
|
388
|
-
if (frame.type !== "host.api.request" && frame.type !== "api_request") return;
|
|
389
|
-
const response = await this.handleApiRequest(frame);
|
|
390
|
-
this.send(response);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async handleApiRequest(frame: DialoutApiRequest): Promise<DialoutApiResponse> {
|
|
394
|
-
try {
|
|
395
|
-
if (!this.localApi) {
|
|
396
|
-
if (!this.options.openLocalApi) throw new EdgeBookError("local_api_disabled", "This dial-out client does not serve local API requests");
|
|
397
|
-
this.localApi = await openLocalApi(this.store);
|
|
398
|
-
}
|
|
399
|
-
const method = (frame.method || "GET").toUpperCase();
|
|
400
|
-
const response = await fetch(apiUrl(this.localApi.baseUrl, frame), {
|
|
401
|
-
method,
|
|
402
|
-
headers: {
|
|
403
|
-
"content-type": "application/json",
|
|
404
|
-
"x-openclaw-session": this.localApi.sessionId,
|
|
405
|
-
"x-openclaw-csrf": this.localApi.csrf
|
|
406
|
-
},
|
|
407
|
-
body: requestBody(frame, method)
|
|
408
|
-
});
|
|
409
|
-
const bodyBuffer = Buffer.from(await response.arrayBuffer());
|
|
410
|
-
return {
|
|
411
|
-
type: "api_response",
|
|
412
|
-
id: frame.id || frame.request_id || "",
|
|
413
|
-
request_id: frame.request_id || frame.id || "",
|
|
414
|
-
status: response.status,
|
|
415
|
-
headers: { "content-type": response.headers.get("content-type") || "application/json; charset=utf-8" },
|
|
416
|
-
body_b64: bodyBuffer.toString("base64")
|
|
417
|
-
};
|
|
418
|
-
} catch (error) {
|
|
419
|
-
const body = {
|
|
420
|
-
ok: false,
|
|
421
|
-
code: error instanceof EdgeBookError ? error.code : "internal_error",
|
|
422
|
-
error: error instanceof Error ? error.message : String(error)
|
|
423
|
-
};
|
|
424
|
-
return {
|
|
425
|
-
type: "api_response",
|
|
426
|
-
id: frame.id || frame.request_id || "",
|
|
427
|
-
request_id: frame.request_id || frame.id || "",
|
|
428
|
-
status: error instanceof EdgeBookError ? 400 : 500,
|
|
429
|
-
headers: { "content-type": "application/json; charset=utf-8" },
|
|
430
|
-
body_b64: Buffer.from(JSON.stringify(body), "utf8").toString("base64"),
|
|
431
|
-
body
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export async function sendPairRegistration(options: DialoutClientOptions & { ttlMs?: number }): Promise<PairRegistration> {
|
|
438
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
439
|
-
await client.start();
|
|
440
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
441
|
-
const registration = await client.pair(options.ttlMs ?? DEFAULT_PAIR_TTL_MS);
|
|
442
|
-
await client.stop();
|
|
443
|
-
return registration;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export async function sendSessionsRevoke(options: DialoutClientOptions): Promise<SessionsRevokeFrame> {
|
|
447
|
-
const client = new EdgeBookDialoutClient({ ...options, reconnect: false, openLocalApi: false });
|
|
448
|
-
await client.start();
|
|
449
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
450
|
-
const { frame, ack } = await client.revokeSessionsAndWait();
|
|
451
|
-
await client.stop();
|
|
452
|
-
return { ...frame, channel_id: ack.channel_id } as SessionsRevokeFrame & { channel_id?: string };
|
|
453
|
-
}
|