@castlekit/castle 0.1.5 → 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.
Files changed (68) hide show
  1. package/drizzle.config.ts +7 -0
  2. package/next.config.ts +1 -0
  3. package/package.json +25 -4
  4. package/src/app/api/avatars/[id]/route.ts +122 -25
  5. package/src/app/api/openclaw/agents/[id]/avatar/route.ts +216 -0
  6. package/src/app/api/openclaw/agents/route.ts +77 -41
  7. package/src/app/api/openclaw/agents/status/route.ts +55 -0
  8. package/src/app/api/openclaw/chat/attachments/route.ts +230 -0
  9. package/src/app/api/openclaw/chat/channels/route.ts +214 -0
  10. package/src/app/api/openclaw/chat/route.ts +272 -0
  11. package/src/app/api/openclaw/chat/search/route.ts +149 -0
  12. package/src/app/api/openclaw/chat/storage/route.ts +75 -0
  13. package/src/app/api/openclaw/config/route.ts +45 -4
  14. package/src/app/api/openclaw/events/route.ts +31 -2
  15. package/src/app/api/openclaw/logs/route.ts +20 -5
  16. package/src/app/api/openclaw/restart/route.ts +12 -4
  17. package/src/app/api/openclaw/session/status/route.ts +42 -0
  18. package/src/app/api/settings/avatar/route.ts +190 -0
  19. package/src/app/api/settings/route.ts +88 -0
  20. package/src/app/chat/[channelId]/error-boundary.tsx +64 -0
  21. package/src/app/chat/[channelId]/page.tsx +305 -0
  22. package/src/app/chat/layout.tsx +96 -0
  23. package/src/app/chat/page.tsx +52 -0
  24. package/src/app/globals.css +89 -2
  25. package/src/app/layout.tsx +7 -1
  26. package/src/app/page.tsx +147 -28
  27. package/src/app/settings/page.tsx +300 -0
  28. package/src/cli/onboarding.ts +202 -37
  29. package/src/components/chat/agent-mention-popup.tsx +89 -0
  30. package/src/components/chat/archived-channels.tsx +190 -0
  31. package/src/components/chat/channel-list.tsx +140 -0
  32. package/src/components/chat/chat-input.tsx +310 -0
  33. package/src/components/chat/create-channel-dialog.tsx +171 -0
  34. package/src/components/chat/markdown-content.tsx +205 -0
  35. package/src/components/chat/message-bubble.tsx +152 -0
  36. package/src/components/chat/message-list.tsx +508 -0
  37. package/src/components/chat/message-queue.tsx +68 -0
  38. package/src/components/chat/session-divider.tsx +61 -0
  39. package/src/components/chat/session-stats-panel.tsx +139 -0
  40. package/src/components/chat/storage-indicator.tsx +76 -0
  41. package/src/components/layout/sidebar.tsx +126 -45
  42. package/src/components/layout/user-menu.tsx +29 -4
  43. package/src/components/providers/presence-provider.tsx +8 -0
  44. package/src/components/providers/search-provider.tsx +81 -0
  45. package/src/components/search/search-dialog.tsx +269 -0
  46. package/src/components/ui/avatar.tsx +11 -9
  47. package/src/components/ui/dialog.tsx +10 -4
  48. package/src/components/ui/tooltip.tsx +25 -8
  49. package/src/components/ui/twemoji-text.tsx +37 -0
  50. package/src/lib/api-security.ts +188 -0
  51. package/src/lib/config.ts +36 -4
  52. package/src/lib/date-utils.ts +79 -0
  53. package/src/lib/db/__tests__/queries.test.ts +318 -0
  54. package/src/lib/db/index.ts +642 -0
  55. package/src/lib/db/queries.ts +1017 -0
  56. package/src/lib/db/schema.ts +160 -0
  57. package/src/lib/device-identity.ts +303 -0
  58. package/src/lib/gateway-connection.ts +273 -36
  59. package/src/lib/hooks/use-agent-status.ts +251 -0
  60. package/src/lib/hooks/use-chat.ts +775 -0
  61. package/src/lib/hooks/use-openclaw.ts +105 -70
  62. package/src/lib/hooks/use-search.ts +113 -0
  63. package/src/lib/hooks/use-session-stats.ts +57 -0
  64. package/src/lib/hooks/use-user-settings.ts +46 -0
  65. package/src/lib/types/chat.ts +186 -0
  66. package/src/lib/types/search.ts +60 -0
  67. package/src/middleware.ts +52 -0
  68. package/vitest.config.ts +13 -0
@@ -1,7 +1,11 @@
1
1
  import WebSocket from "ws";
2
2
  import { EventEmitter } from "events";
3
3
  import { randomUUID } from "crypto";
4
+ import { execSync } from "child_process";
5
+ import { readFileSync, realpathSync } from "fs";
6
+ import { dirname, join } from "path";
4
7
  import { getGatewayUrl, readOpenClawToken, readConfig, configExists } from "./config";
8
+ import { getOrCreateIdentity, signDeviceAuth, saveDeviceToken, getDeviceToken, clearDeviceToken } from "./device-identity";
5
9
 
6
10
  // ============================================================================
7
11
  // Types
@@ -44,7 +48,7 @@ interface PendingRequest {
44
48
  timer: ReturnType<typeof setTimeout>;
45
49
  }
46
50
 
47
- export type ConnectionState = "disconnected" | "connecting" | "connected" | "error";
51
+ export type ConnectionState = "disconnected" | "connecting" | "connected" | "pairing" | "error";
48
52
 
49
53
  export interface GatewayEvent {
50
54
  event: string;
@@ -52,6 +56,21 @@ export interface GatewayEvent {
52
56
  seq?: number;
53
57
  }
54
58
 
59
+ // ============================================================================
60
+ // Helpers
61
+ // ============================================================================
62
+
63
+ const MAX_MESSAGE_SIZE = 10 * 1024 * 1024; // 10MB
64
+ const MAX_NONCE_LENGTH = 1024;
65
+
66
+ /** Strip tokens and key material from strings before logging. */
67
+ function sanitize(str: string): string {
68
+ return str
69
+ .replace(/rew_[a-f0-9]+/gi, "rew_***")
70
+ .replace(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, "[REDACTED KEY]")
71
+ .replace(/[a-f0-9]{32,}/gi, (m) => m.slice(0, 8) + "***");
72
+ }
73
+
55
74
  // ============================================================================
56
75
  // Singleton Gateway Connection
57
76
  // ============================================================================
@@ -69,6 +88,10 @@ class GatewayConnection extends EventEmitter {
69
88
  private _serverInfo: { version?: string; connId?: string } = {};
70
89
  private _features: { methods?: string[]; events?: string[] } = {};
71
90
  private shouldReconnect = true;
91
+ // Device auth: set to true after "device identity mismatch" to fall back to token-only
92
+ private _skipDeviceAuth = false;
93
+ // Avatar URL mapping: hash → original Gateway URL (for proxying)
94
+ private _avatarUrls = new Map<string, string>();
72
95
 
73
96
  get state(): ConnectionState {
74
97
  return this._state;
@@ -78,6 +101,16 @@ class GatewayConnection extends EventEmitter {
78
101
  return this._serverInfo;
79
102
  }
80
103
 
104
+ /** Store a mapping from avatar hash to its original Gateway URL */
105
+ setAvatarUrl(hash: string, originalUrl: string): void {
106
+ this._avatarUrls.set(hash, originalUrl);
107
+ }
108
+
109
+ /** Get the original Gateway URL for an avatar hash */
110
+ getAvatarUrl(hash: string): string | null {
111
+ return this._avatarUrls.get(hash) || null;
112
+ }
113
+
81
114
  get isConnected(): boolean {
82
115
  return this._state === "connected";
83
116
  }
@@ -94,6 +127,7 @@ class GatewayConnection extends EventEmitter {
94
127
  start(): void {
95
128
  if (this._state === "connecting" || this._state === "connected") return;
96
129
  this.shouldReconnect = true;
130
+ this._skipDeviceAuth = false; // Reset — try device auth on fresh start
97
131
  this.connect();
98
132
  }
99
133
 
@@ -119,7 +153,7 @@ class GatewayConnection extends EventEmitter {
119
153
  try {
120
154
  this.ws = new WebSocket(url);
121
155
  } catch (err) {
122
- console.error("[Gateway] Failed to create WebSocket:", err);
156
+ console.error("[Gateway] Failed to create WebSocket:", (err as Error).message);
123
157
  this._state = "error";
124
158
  this.emit("stateChange", this._state);
125
159
  this.scheduleReconnect();
@@ -132,66 +166,224 @@ class GatewayConnection extends EventEmitter {
132
166
  this.scheduleReconnect();
133
167
  }, this.connectTimeout);
134
168
 
169
+ // Load device identity for auth (skip if previously rejected)
170
+ let deviceIdentity: { deviceId: string; publicKey: string } | null = null;
171
+ if (!this._skipDeviceAuth) {
172
+ try {
173
+ const identity = getOrCreateIdentity();
174
+ deviceIdentity = { deviceId: identity.deviceId, publicKey: identity.publicKey };
175
+ } catch (err) {
176
+ console.warn("[Gateway] Could not load device identity:", (err as Error).message);
177
+ }
178
+ } else {
179
+ console.log("[Gateway] Device auth disabled — using token-only");
180
+ }
181
+
182
+ // Check for a saved device token from previous pairing
183
+ const savedDeviceToken = getDeviceToken();
184
+
135
185
  this.ws.on("open", () => {
136
- // Build connect handshake
137
186
  const connectId = randomUUID();
138
- const connectFrame: RequestFrame = {
139
- type: "req",
140
- id: connectId,
141
- method: "connect",
142
- params: {
187
+
188
+ // Build connect frame per Gateway protocol:
189
+ // - Initial connect: token-only (no device field)
190
+ // - Challenge response: include device { id, publicKey, signature, signedAt, nonce }
191
+ // - Reconnect with deviceToken: use deviceToken as auth.token
192
+ //
193
+ // The device field is ONLY sent when responding to connect.challenge,
194
+ // because the Gateway requires signature + signedAt whenever device is present.
195
+ let challengeReceived = false;
196
+
197
+ // Connection identity constants — must match what's signed
198
+ const CLIENT_ID = "gateway-client";
199
+ const CLIENT_MODE = "backend";
200
+ const ROLE = "operator";
201
+ const SCOPES = ["operator.admin"];
202
+ const authToken = savedDeviceToken || token;
203
+
204
+ const buildConnectFrame = (challenge?: {
205
+ nonce: string;
206
+ signature: string;
207
+ signedAt: number;
208
+ }): RequestFrame => {
209
+ const params: Record<string, unknown> = {
143
210
  minProtocol: 3,
144
211
  maxProtocol: 3,
145
212
  client: {
146
- id: "gateway-client",
213
+ id: CLIENT_ID,
147
214
  displayName: "Castle",
148
215
  version: "0.0.1",
149
216
  platform: process.platform,
150
- mode: "backend",
217
+ mode: CLIENT_MODE,
151
218
  },
152
- auth: { token },
153
- role: "operator",
154
- scopes: ["operator.admin"],
219
+ auth: { token: authToken },
220
+ role: ROLE,
221
+ scopes: SCOPES,
155
222
  caps: [],
156
- },
223
+ };
224
+
225
+ // Only include device when responding to a challenge (with signature)
226
+ if (challenge && deviceIdentity) {
227
+ params.device = {
228
+ id: deviceIdentity.deviceId,
229
+ publicKey: deviceIdentity.publicKey,
230
+ signature: challenge.signature,
231
+ nonce: challenge.nonce,
232
+ signedAt: challenge.signedAt,
233
+ };
234
+ }
235
+
236
+ return { type: "req", id: connectId, method: "connect", params };
157
237
  };
158
238
 
159
- // Handle handshake messages (may include connect.challenge events)
239
+ // Handle handshake messages
160
240
  const onHandshakeMessage = (data: WebSocket.RawData) => {
241
+ const rawSize = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data.toString());
242
+ if (rawSize > MAX_MESSAGE_SIZE) {
243
+ console.error(`[Gateway] Message too large (${rawSize} bytes) — ignoring`);
244
+ return;
245
+ }
161
246
  try {
162
247
  const msg = JSON.parse(data.toString());
163
248
 
164
- // Handle connect.challenge event -- re-send connect with nonce
249
+ // Handle connect.challenge sign nonce and re-send connect with device identity
165
250
  if (msg.type === "event" && msg.event === "connect.challenge") {
166
- // For now we don't support device-signed nonce challenges
167
- // Just proceed with the connect
251
+ const nonce = msg.payload?.nonce;
252
+ if (nonce && typeof nonce === "string" && nonce.length > MAX_NONCE_LENGTH) {
253
+ console.error(`[Gateway] Challenge nonce too large (${nonce.length} bytes) — rejecting`);
254
+ return;
255
+ }
256
+ if (nonce && typeof nonce === "string" && deviceIdentity) {
257
+ challengeReceived = true;
258
+ console.log("[Gateway] Challenge received — signing with device key");
259
+ try {
260
+ const { signature, signedAt } = signDeviceAuth({
261
+ nonce,
262
+ clientId: CLIENT_ID,
263
+ clientMode: CLIENT_MODE,
264
+ role: ROLE,
265
+ scopes: SCOPES,
266
+ token: authToken!,
267
+ });
268
+ const challengeFrame = buildConnectFrame({ nonce, signature, signedAt });
269
+ this.ws?.send(JSON.stringify(challengeFrame));
270
+ } catch (err) {
271
+ console.error("[Gateway] Failed to sign challenge:", (err as Error).message);
272
+ challengeReceived = false;
273
+ }
274
+ } else {
275
+ console.log("[Gateway] Challenge received but no device identity — skipping");
276
+ }
277
+ return;
278
+ }
279
+
280
+ // Handle device.pairing.required — waiting for operator approval
281
+ if (msg.type === "event" && msg.event === "device.pairing.required") {
282
+ console.log("[Gateway] Device pairing approval required");
283
+ console.log("[Gateway] Approve this device in your OpenClaw dashboard to continue...");
284
+ this._state = "pairing";
285
+ this.emit("stateChange", this._state);
286
+ this.emit("pairingRequired", msg.payload);
287
+ this.emit("gatewayEvent", {
288
+ event: msg.event,
289
+ payload: msg.payload,
290
+ seq: msg.seq,
291
+ } as GatewayEvent);
168
292
  return;
169
293
  }
170
294
 
171
- // Standard response frame to our connect request
295
+ // Handle device.pairing.approved save the device token
296
+ if (msg.type === "event" && msg.event === "device.pairing.approved") {
297
+ const approvedToken = msg.payload?.deviceToken;
298
+ if (approvedToken && typeof approvedToken === "string") {
299
+ console.log("[Gateway] Device pairing approved — saving token");
300
+ try {
301
+ saveDeviceToken(approvedToken, url);
302
+ } catch (err) {
303
+ console.error("[Gateway] Failed to save device token:", (err as Error).message);
304
+ }
305
+ }
306
+ this.emit("pairingApproved", msg.payload);
307
+ this.emit("gatewayEvent", {
308
+ event: msg.event,
309
+ payload: msg.payload,
310
+ seq: msg.seq,
311
+ } as GatewayEvent);
312
+ return;
313
+ }
314
+
315
+ // Standard response to our connect request
172
316
  if (msg.type === "res" && msg.id === connectId) {
317
+ // If we already sent a challenge response, ignore error from the
318
+ // initial (token-only) connect — the signed response is in flight.
319
+ if (!msg.ok && challengeReceived) {
320
+ console.log("[Gateway] Ignoring error from initial connect — challenge response pending");
321
+ return;
322
+ }
323
+
173
324
  clearTimeout(connectTimer);
174
325
 
175
326
  if (msg.ok) {
176
- // hello-ok is embedded in the payload
177
327
  const helloOk = msg.payload || {};
328
+
329
+ // Save deviceToken from hello-ok (may be at payload.auth.deviceToken
330
+ // or payload.deviceToken depending on Gateway version)
331
+ const helloDeviceToken =
332
+ helloOk.auth?.deviceToken || helloOk.deviceToken;
333
+ if (helloDeviceToken && typeof helloDeviceToken === "string") {
334
+ try {
335
+ saveDeviceToken(helloDeviceToken, url);
336
+ console.log("[Gateway] Device token received and saved");
337
+ } catch (err) {
338
+ console.error("[Gateway] Failed to save device token:", (err as Error).message);
339
+ }
340
+ }
341
+
178
342
  this._state = "connected";
179
343
  this._serverInfo = helloOk.server || {};
344
+ // If Gateway reports "dev" or missing version, read from installed package.json
345
+ if (!this._serverInfo.version || this._serverInfo.version === "dev") {
346
+ try {
347
+ const bin = execSync("which openclaw", { timeout: 2000, encoding: "utf-8" }).trim();
348
+ const real = realpathSync(bin);
349
+ const pkgPath = join(dirname(real), "package.json");
350
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
351
+ if (pkg.version) this._serverInfo.version = pkg.version;
352
+ } catch { /* keep whatever the Gateway reported */ }
353
+ }
180
354
  this._features = helloOk.features || {};
181
355
  this.reconnectAttempts = 0;
182
356
  this.emit("stateChange", this._state);
183
357
  this.emit("connected", helloOk);
184
- console.log(`[Gateway] Connected to OpenClaw v${helloOk.server?.version || "unknown"}`);
358
+ console.log(`[Gateway] Connected to OpenClaw ${this._serverInfo.version || "unknown"}`);
185
359
  // Switch to normal message handler
186
360
  this.ws?.off("message", onHandshakeMessage);
187
361
  this.ws?.on("message", this.onMessage.bind(this));
188
362
  } else {
363
+ const errCode = msg.error?.code;
189
364
  const errMsg = msg.error?.message || "Connect rejected";
190
- console.error(`[Gateway] Connect failed: ${errMsg}`);
365
+ console.error(`[Gateway] Connect failed: ${sanitize(errMsg)}`);
191
366
  this.ws?.off("message", onHandshakeMessage);
192
367
  this.cleanup();
193
- // Don't reconnect on auth errors
194
- if (msg.error?.code === "auth_failed") {
368
+
369
+ if (errCode === "auth_failed") {
370
+ // If we were using a stale device token, clear it and retry
371
+ // with the gateway token instead
372
+ if (savedDeviceToken) {
373
+ console.log("[Gateway] Device token rejected — clearing and retrying with gateway token");
374
+ clearDeviceToken();
375
+ this._skipDeviceAuth = false;
376
+ this.reconnectAttempts = 0;
377
+ this.scheduleReconnect();
378
+ } else {
379
+ // Real auth failure — gateway token is invalid
380
+ this._state = "error";
381
+ this.emit("stateChange", this._state);
382
+ this.emit("authError", msg.error);
383
+ }
384
+ } else if (errCode === "protocol_mismatch" || errCode === "protocol_unsupported") {
385
+ // Permanent failure — don't retry with the same protocol
386
+ console.error("[Gateway] Protocol version not supported by this Gateway");
195
387
  this._state = "error";
196
388
  this.emit("stateChange", this._state);
197
389
  this.emit("authError", msg.error);
@@ -202,7 +394,7 @@ class GatewayConnection extends EventEmitter {
202
394
  return;
203
395
  }
204
396
 
205
- // Forward any events that arrive during handshake
397
+ // Forward any other events during handshake
206
398
  if (msg.type === "event") {
207
399
  this.emit("gatewayEvent", {
208
400
  event: msg.event,
@@ -211,12 +403,25 @@ class GatewayConnection extends EventEmitter {
211
403
  } as GatewayEvent);
212
404
  }
213
405
  } catch (err) {
214
- console.error("[Gateway] Failed to parse handshake message:", err);
406
+ console.error("[Gateway] Failed to parse handshake message:", (err as Error).message);
215
407
  }
216
408
  };
217
409
 
218
410
  this.ws!.on("message", onHandshakeMessage);
219
- this.ws!.send(JSON.stringify(connectFrame));
411
+ this.ws!.send(JSON.stringify(buildConnectFrame()));
412
+ });
413
+
414
+ // Handle WebSocket upgrade failures (e.g. corporate proxies blocking WS)
415
+ this.ws.on("unexpected-response", (_req, res) => {
416
+ clearTimeout(connectTimer);
417
+ console.error(`[Gateway] WebSocket upgrade failed (HTTP ${res.statusCode})`);
418
+ if (res.statusCode === 401 || res.statusCode === 403) {
419
+ console.error("[Gateway] Authentication rejected at HTTP level");
420
+ } else if (res.statusCode === 502 || res.statusCode === 503) {
421
+ console.error("[Gateway] Gateway may be behind a reverse proxy that doesn't support WebSocket");
422
+ }
423
+ this.cleanup();
424
+ this.scheduleReconnect();
220
425
  });
221
426
 
222
427
  this.ws.on("error", (err) => {
@@ -229,15 +434,32 @@ class GatewayConnection extends EventEmitter {
229
434
  this.ws.on("close", (code, reason) => {
230
435
  clearTimeout(connectTimer);
231
436
  const wasConnected = this._state === "connected";
437
+ const reasonStr = reason?.toString() || "none";
232
438
  this.cleanup();
439
+
440
+ // If Gateway rejected device auth (identity mismatch, bad signature, etc),
441
+ // fall back to token-only auth
442
+ if (code === 1008 && !this._skipDeviceAuth) {
443
+ console.log(`[Gateway] Device auth rejected (${reasonStr}) — retrying with token-only auth`);
444
+ this._skipDeviceAuth = true;
445
+ this.reconnectAttempts = 0;
446
+ this.scheduleReconnect();
447
+ return;
448
+ }
449
+
233
450
  if (wasConnected) {
234
- console.log(`[Gateway] Disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`);
451
+ console.log(`[Gateway] Disconnected (code: ${code}, reason: ${reasonStr})`);
235
452
  }
236
453
  this.scheduleReconnect();
237
454
  });
238
455
  }
239
456
 
240
457
  private onMessage(data: WebSocket.RawData): void {
458
+ const rawSize = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data.toString());
459
+ if (rawSize > MAX_MESSAGE_SIZE) {
460
+ console.error(`[Gateway] Message too large (${rawSize} bytes) — ignoring`);
461
+ return;
462
+ }
241
463
  let msg: GatewayFrame;
242
464
  try {
243
465
  msg = JSON.parse(data.toString());
@@ -308,14 +530,19 @@ class GatewayConnection extends EventEmitter {
308
530
  // --------------------------------------------------------------------------
309
531
 
310
532
  private resolveToken(): string | null {
311
- // 1. Castle config token
533
+ // 1. Saved device token from previous pairing
534
+ const deviceToken = getDeviceToken();
535
+ if (deviceToken) return deviceToken;
536
+
537
+ // 2. Castle config token
312
538
  if (configExists()) {
313
539
  const config = readConfig();
314
540
  if (config.openclaw.gateway_token) {
315
541
  return config.openclaw.gateway_token;
316
542
  }
317
543
  }
318
- // 2. Auto-detect from OpenClaw config
544
+
545
+ // 3. Auto-detect from OpenClaw config
319
546
  return readOpenClawToken();
320
547
  }
321
548
 
@@ -335,7 +562,7 @@ class GatewayConnection extends EventEmitter {
335
562
  this.pending.delete(id);
336
563
  }
337
564
 
338
- if (this._state !== "error") {
565
+ if (this._state !== "error" && this._state !== "pairing") {
339
566
  this._state = "disconnected";
340
567
  this.emit("stateChange", this._state);
341
568
  }
@@ -366,16 +593,26 @@ class GatewayConnection extends EventEmitter {
366
593
  }
367
594
 
368
595
  // ============================================================================
369
- // Singleton export
596
+ // Singleton export (uses globalThis to survive HMR in dev mode)
370
597
  // ============================================================================
371
598
 
372
- let _gateway: GatewayConnection | null = null;
599
+ const GATEWAY_KEY = "__castle_gateway__" as const;
600
+
601
+ function getGlobalGateway(): GatewayConnection | null {
602
+ return (globalThis as Record<string, unknown>)[GATEWAY_KEY] as GatewayConnection | null ?? null;
603
+ }
604
+
605
+ function setGlobalGateway(gw: GatewayConnection): void {
606
+ (globalThis as Record<string, unknown>)[GATEWAY_KEY] = gw;
607
+ }
373
608
 
374
609
  export function getGateway(): GatewayConnection {
375
- if (!_gateway) {
376
- _gateway = new GatewayConnection();
610
+ let gw = getGlobalGateway();
611
+ if (!gw) {
612
+ gw = new GatewayConnection();
613
+ setGlobalGateway(gw);
377
614
  }
378
- return _gateway;
615
+ return gw;
379
616
  }
380
617
 
381
618
  /**