@elizaos/capacitor-gateway 1.0.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.
@@ -0,0 +1,419 @@
1
+ import { WebPlugin } from "@capacitor/core";
2
+ /**
3
+ * Generate a UUID v4
4
+ */
5
+ function generateUUID() {
6
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
7
+ return crypto.randomUUID();
8
+ }
9
+ if (typeof crypto !== "undefined" &&
10
+ typeof crypto.getRandomValues === "function") {
11
+ const bytes = new Uint8Array(16);
12
+ crypto.getRandomValues(bytes);
13
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
14
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
15
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
16
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
17
+ }
18
+ throw new Error("No secure random source available for UUID generation");
19
+ }
20
+ const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
21
+ const getString = (value) => typeof value === "string" ? value : undefined;
22
+ const getNumber = (value) => typeof value === "number" ? value : undefined;
23
+ const getBoolean = (value) => typeof value === "boolean" ? value : undefined;
24
+ const toStringArray = (value) => Array.isArray(value)
25
+ ? value.filter((item) => typeof item === "string")
26
+ : [];
27
+ const parseGatewayError = (value) => {
28
+ if (!value || !isJsonObject(value))
29
+ return undefined;
30
+ const code = getString(value.code);
31
+ const message = getString(value.message);
32
+ if (!code || !message)
33
+ return undefined;
34
+ return {
35
+ code,
36
+ message,
37
+ details: value.details,
38
+ };
39
+ };
40
+ /**
41
+ * Web implementation of the Gateway Plugin
42
+ *
43
+ * Uses browser WebSocket API for connectivity.
44
+ * Note: Web platform cannot perform Bonjour/mDNS discovery.
45
+ */
46
+ export class GatewayWeb extends WebPlugin {
47
+ constructor() {
48
+ super(...arguments);
49
+ this.ws = null;
50
+ this.pending = new Map();
51
+ this.options = null;
52
+ this.sessionId = null;
53
+ this.protocol = null;
54
+ this.role = null;
55
+ this.scopes = [];
56
+ this.methods = [];
57
+ this.events = [];
58
+ this.lastSeq = null;
59
+ this.reconnectTimer = null;
60
+ this.backoffMs = 800;
61
+ this.closed = false;
62
+ this.connectResolve = null;
63
+ this.connectReject = null;
64
+ }
65
+ /**
66
+ * Start gateway discovery (not supported on web)
67
+ *
68
+ * On web platforms, Bonjour/mDNS discovery is not available.
69
+ * Returns an empty list of gateways.
70
+ */
71
+ async startDiscovery() {
72
+ return {
73
+ gateways: [],
74
+ status: "Discovery not supported on web platform",
75
+ };
76
+ }
77
+ /**
78
+ * Stop gateway discovery (no-op on web)
79
+ */
80
+ async stopDiscovery() {
81
+ // No-op on web
82
+ }
83
+ /**
84
+ * Get discovered gateways (always empty on web)
85
+ */
86
+ async getDiscoveredGateways() {
87
+ return {
88
+ gateways: [],
89
+ status: "Discovery not supported on web platform",
90
+ };
91
+ }
92
+ /**
93
+ * Connect to a Gateway server
94
+ */
95
+ async connect(options) {
96
+ // Close existing connection if any
97
+ if (this.ws) {
98
+ this.closed = true;
99
+ this.ws.close();
100
+ this.ws = null;
101
+ }
102
+ this.options = options;
103
+ this.closed = false;
104
+ this.backoffMs = 800;
105
+ return new Promise((resolve, reject) => {
106
+ this.connectResolve = resolve;
107
+ this.connectReject = reject;
108
+ this.establishConnection();
109
+ });
110
+ }
111
+ /**
112
+ * Establish WebSocket connection
113
+ */
114
+ establishConnection() {
115
+ if (this.closed || !this.options) {
116
+ return;
117
+ }
118
+ this.notifyStateChange("connecting");
119
+ this.ws = new WebSocket(this.options.url);
120
+ this.ws.addEventListener("open", () => {
121
+ this.sendConnectFrame();
122
+ });
123
+ this.ws.addEventListener("message", (event) => {
124
+ this.handleMessage(String(event.data));
125
+ });
126
+ this.ws.addEventListener("close", (event) => {
127
+ const reason = event.reason || "Connection closed";
128
+ this.handleClose(event.code, reason);
129
+ });
130
+ this.ws.addEventListener("error", (event) => {
131
+ console.warn("[Gateway] WebSocket error:", event);
132
+ });
133
+ }
134
+ /**
135
+ * Send the connect frame to authenticate
136
+ */
137
+ sendConnectFrame() {
138
+ if (!this.ws || !this.options || this.ws.readyState !== WebSocket.OPEN) {
139
+ return;
140
+ }
141
+ const auth = {};
142
+ if (this.options.token) {
143
+ auth.token = this.options.token;
144
+ }
145
+ if (this.options.password) {
146
+ auth.password = this.options.password;
147
+ }
148
+ const params = {
149
+ minProtocol: 3,
150
+ maxProtocol: 3,
151
+ client: {
152
+ id: this.options.clientName || "eliza-capacitor",
153
+ version: this.options.clientVersion || "1.0.0",
154
+ platform: this.getPlatform(),
155
+ mode: "ui",
156
+ },
157
+ role: this.options.role || "operator",
158
+ scopes: this.options.scopes || ["operator.admin"],
159
+ caps: [],
160
+ auth,
161
+ };
162
+ const frame = {
163
+ type: "req",
164
+ id: generateUUID(),
165
+ method: "connect",
166
+ params,
167
+ };
168
+ this.ws.send(JSON.stringify(frame));
169
+ // Set up timeout for connect response
170
+ const timeout = setTimeout(() => {
171
+ if (this.connectReject) {
172
+ this.connectReject(new Error("Connection timeout"));
173
+ this.connectReject = null;
174
+ this.connectResolve = null;
175
+ }
176
+ }, 30000);
177
+ this.pending.set(frame.id, {
178
+ resolve: (result) => {
179
+ clearTimeout(timeout);
180
+ if (result.ok && result.payload && isJsonObject(result.payload)) {
181
+ this.handleHelloOk(result.payload);
182
+ }
183
+ else {
184
+ if (this.connectReject) {
185
+ this.connectReject(new Error(result.error?.message || "Connection failed"));
186
+ }
187
+ }
188
+ this.connectReject = null;
189
+ this.connectResolve = null;
190
+ },
191
+ reject: (error) => {
192
+ clearTimeout(timeout);
193
+ if (this.connectReject) {
194
+ this.connectReject(error);
195
+ }
196
+ this.connectReject = null;
197
+ this.connectResolve = null;
198
+ },
199
+ timeout,
200
+ });
201
+ }
202
+ /**
203
+ * Handle successful hello response
204
+ */
205
+ handleHelloOk(hello) {
206
+ const protocol = getNumber(hello.protocol);
207
+ const auth = isJsonObject(hello.auth) ? hello.auth : null;
208
+ const features = isJsonObject(hello.features) ? hello.features : null;
209
+ this.sessionId = generateUUID();
210
+ this.protocol = protocol ?? null;
211
+ this.role = getString(auth?.role) || this.options?.role || "operator";
212
+ this.scopes = toStringArray(auth?.scopes);
213
+ this.methods = toStringArray(features?.methods);
214
+ this.events = toStringArray(features?.events);
215
+ this.backoffMs = 800;
216
+ this.notifyStateChange("connected");
217
+ if (this.connectResolve) {
218
+ this.connectResolve({
219
+ connected: true,
220
+ sessionId: this.sessionId,
221
+ protocol: this.protocol ?? undefined,
222
+ methods: this.methods,
223
+ events: this.events,
224
+ role: this.role,
225
+ scopes: this.scopes,
226
+ });
227
+ }
228
+ }
229
+ /**
230
+ * Handle incoming WebSocket message
231
+ */
232
+ handleMessage(raw) {
233
+ let parsedValue;
234
+ try {
235
+ parsedValue = JSON.parse(raw);
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ if (!isJsonObject(parsedValue)) {
241
+ return;
242
+ }
243
+ const frameType = getString(parsedValue.type);
244
+ if (!frameType) {
245
+ return;
246
+ }
247
+ // Handle response frames
248
+ if (frameType === "res") {
249
+ const id = getString(parsedValue.id);
250
+ if (!id)
251
+ return;
252
+ const pending = this.pending.get(id);
253
+ if (pending) {
254
+ this.pending.delete(id);
255
+ clearTimeout(pending.timeout);
256
+ pending.resolve({
257
+ ok: getBoolean(parsedValue.ok) ?? false,
258
+ payload: parsedValue.payload,
259
+ error: parseGatewayError(parsedValue.error),
260
+ });
261
+ }
262
+ return;
263
+ }
264
+ // Handle event frames
265
+ if (frameType === "event") {
266
+ const event = getString(parsedValue.event);
267
+ if (!event)
268
+ return;
269
+ const payload = parsedValue.payload;
270
+ const seq = getNumber(parsedValue.seq);
271
+ // Check for sequence gap
272
+ if (seq !== undefined &&
273
+ this.lastSeq !== null &&
274
+ seq > this.lastSeq + 1) {
275
+ console.warn(`[Gateway] Event sequence gap: expected ${this.lastSeq + 1}, got ${seq}`);
276
+ }
277
+ if (seq !== undefined) {
278
+ this.lastSeq = seq;
279
+ }
280
+ // Emit the event
281
+ this.notifyListeners("gatewayEvent", {
282
+ event,
283
+ payload,
284
+ seq,
285
+ });
286
+ return;
287
+ }
288
+ }
289
+ /**
290
+ * Handle WebSocket close
291
+ */
292
+ handleClose(code, reason) {
293
+ this.ws = null;
294
+ // Reject all pending requests
295
+ for (const [id, pending] of this.pending) {
296
+ clearTimeout(pending.timeout);
297
+ pending.reject(new Error(`Connection closed: ${reason}`));
298
+ this.pending.delete(id);
299
+ }
300
+ if (this.closed) {
301
+ this.notifyStateChange("disconnected", reason);
302
+ return;
303
+ }
304
+ // Attempt reconnection
305
+ this.notifyStateChange("reconnecting", reason);
306
+ this.notifyListeners("error", {
307
+ message: `Connection lost: ${reason}`,
308
+ code: String(code),
309
+ willRetry: true,
310
+ });
311
+ this.scheduleReconnect();
312
+ }
313
+ /**
314
+ * Schedule a reconnection attempt
315
+ */
316
+ scheduleReconnect() {
317
+ if (this.closed || this.reconnectTimer) {
318
+ return;
319
+ }
320
+ this.reconnectTimer = setTimeout(() => {
321
+ this.reconnectTimer = null;
322
+ this.backoffMs = Math.min(this.backoffMs * 1.7, 15000);
323
+ this.establishConnection();
324
+ }, this.backoffMs);
325
+ }
326
+ /**
327
+ * Notify state change listeners
328
+ */
329
+ notifyStateChange(state, reason) {
330
+ this.notifyListeners("stateChange", {
331
+ state,
332
+ reason,
333
+ });
334
+ }
335
+ /**
336
+ * Get platform identifier
337
+ */
338
+ getPlatform() {
339
+ if (typeof navigator !== "undefined") {
340
+ return navigator.platform || "web";
341
+ }
342
+ return "web";
343
+ }
344
+ /**
345
+ * Disconnect from the Gateway
346
+ */
347
+ async disconnect() {
348
+ this.closed = true;
349
+ if (this.reconnectTimer) {
350
+ clearTimeout(this.reconnectTimer);
351
+ this.reconnectTimer = null;
352
+ }
353
+ if (this.ws) {
354
+ this.ws.close(1000, "Client disconnect");
355
+ this.ws = null;
356
+ }
357
+ this.sessionId = null;
358
+ this.protocol = null;
359
+ this.notifyStateChange("disconnected", "Client disconnect");
360
+ }
361
+ /**
362
+ * Check if connected
363
+ */
364
+ async isConnected() {
365
+ return {
366
+ connected: this.ws !== null && this.ws.readyState === WebSocket.OPEN,
367
+ };
368
+ }
369
+ /**
370
+ * Send an RPC request
371
+ */
372
+ async send(options) {
373
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
374
+ return {
375
+ ok: false,
376
+ error: {
377
+ code: "NOT_CONNECTED",
378
+ message: "Not connected to gateway",
379
+ },
380
+ };
381
+ }
382
+ const id = generateUUID();
383
+ const frame = {
384
+ type: "req",
385
+ id,
386
+ method: options.method,
387
+ params: options.params || {},
388
+ };
389
+ return new Promise((resolve, reject) => {
390
+ const timeout = setTimeout(() => {
391
+ this.pending.delete(id);
392
+ resolve({
393
+ ok: false,
394
+ error: {
395
+ code: "TIMEOUT",
396
+ message: "Request timed out",
397
+ },
398
+ });
399
+ }, 60000); // 60 second timeout
400
+ this.pending.set(id, {
401
+ resolve,
402
+ reject,
403
+ timeout,
404
+ });
405
+ this.ws?.send(JSON.stringify(frame));
406
+ });
407
+ }
408
+ /**
409
+ * Get connection info
410
+ */
411
+ async getConnectionInfo() {
412
+ return {
413
+ url: this.options?.url || null,
414
+ sessionId: this.sessionId,
415
+ protocol: this.protocol,
416
+ role: this.role,
417
+ };
418
+ }
419
+ }