@gokiteam/goki-dev 0.2.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 (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,582 @@
1
+ import { v4 as uuid } from 'uuid'
2
+ import crypto from 'crypto'
3
+ import {
4
+ BridgeScanResultRecord,
5
+ BridgeScanStatusNotification,
6
+ DcpV2DeviceNotificationPacket,
7
+ DcpV2DeviceResponseForServerPacket,
8
+ DcpV2OnlineServerRequestPacket,
9
+ ErrorCode,
10
+ ForwardPacketRequest,
11
+ ForwardPacketResponse,
12
+ ForwardPacketV2Request,
13
+ ForwardPacketV2Response,
14
+ Interface,
15
+ NotificationType,
16
+ Operation,
17
+ Protocol
18
+ } from '@portalteam/protocols'
19
+ import { Application } from '../configs/Application.js'
20
+ import { Logger } from '../singletons/Logger.js'
21
+ import { Broker } from '../protocols/mqtt/Broker.js'
22
+ import { PublishMessages } from '../emulation/pubsub/Logic.js'
23
+ import { GatewayState } from '../enums/GatewayState.js'
24
+
25
+ class AppGatewayServiceClass {
26
+ constructor () {
27
+ this.state = GatewayState.STOPPED
28
+ this.gatewayDevice = null
29
+ this.masterSecret = null
30
+ this.scanInterval = null
31
+ this.startedAt = null
32
+ this.retryTimer = null
33
+ this.dependencyHealth = {
34
+ deviceNative: { reachable: false, lastChecked: null, error: null },
35
+ deviceSimulator: { reachable: false, lastChecked: null, error: null }
36
+ }
37
+ }
38
+
39
+ get isRunning () {
40
+ return this.state === GatewayState.RUNNING
41
+ }
42
+
43
+ // --- Auto-start and dependency health ---
44
+
45
+ async autoStart () {
46
+ const { autoStart } = Application.appGateway
47
+ if (!autoStart) {
48
+ Logger.log({
49
+ level: 'info',
50
+ message: 'Gateway auto-start is disabled (APP_GATEWAY_AUTO_START=false)'
51
+ })
52
+ return
53
+ }
54
+ Logger.log({
55
+ level: 'info',
56
+ message: 'Gateway auto-start initiated'
57
+ })
58
+ const health = await this.checkDependencyHealth()
59
+ if (health.allHealthy) {
60
+ const result = await this.start()
61
+ if (!result.success) {
62
+ Logger.log({
63
+ level: 'warn',
64
+ message: 'Gateway immediate start failed, entering retry loop',
65
+ data: { error: result.message }
66
+ })
67
+ this.startRetryLoop()
68
+ }
69
+ } else {
70
+ this.startRetryLoop()
71
+ }
72
+ }
73
+
74
+ async checkDependencyHealth () {
75
+ const { deviceNativeUrl, deviceSimulatorUrl } = Application.appGateway
76
+ const check = async (url, name) => {
77
+ try {
78
+ const controller = new AbortController()
79
+ const timeout = setTimeout(() => controller.abort(), 3000)
80
+ const response = await fetch(`${url}/v1/health/liveness`, {
81
+ method: 'GET',
82
+ signal: controller.signal
83
+ })
84
+ clearTimeout(timeout)
85
+ const reachable = response.ok
86
+ this.dependencyHealth[name] = {
87
+ reachable,
88
+ lastChecked: Date.now(),
89
+ error: reachable ? null : `HTTP ${response.status}`
90
+ }
91
+ return reachable
92
+ } catch (error) {
93
+ this.dependencyHealth[name] = {
94
+ reachable: false,
95
+ lastChecked: Date.now(),
96
+ error: error.name === 'AbortError' ? 'Timeout' : error.message
97
+ }
98
+ return false
99
+ }
100
+ }
101
+ const [nativeOk, simulatorOk] = await Promise.all([
102
+ check(deviceNativeUrl, 'deviceNative'),
103
+ check(deviceSimulatorUrl, 'deviceSimulator')
104
+ ])
105
+ return { allHealthy: nativeOk && simulatorOk, deviceNative: nativeOk, deviceSimulator: simulatorOk }
106
+ }
107
+
108
+ startRetryLoop () {
109
+ if (this.retryTimer) return
110
+ const { retryIntervalSeconds } = Application.appGateway
111
+ this.state = GatewayState.WAITING_FOR_DEPENDENCIES
112
+ Logger.log({
113
+ level: 'info',
114
+ message: 'Gateway waiting for dependencies',
115
+ data: {
116
+ retryIntervalSeconds,
117
+ deviceNativeUrl: Application.appGateway.deviceNativeUrl,
118
+ deviceSimulatorUrl: Application.appGateway.deviceSimulatorUrl
119
+ }
120
+ })
121
+ this.retryTimer = setInterval(async () => {
122
+ const health = await this.checkDependencyHealth()
123
+ if (health.allHealthy) {
124
+ Logger.log({
125
+ level: 'info',
126
+ message: 'All dependencies available, starting gateway'
127
+ })
128
+ this.stopRetryLoop()
129
+ const result = await this.start()
130
+ if (!result.success) {
131
+ Logger.log({
132
+ level: 'warn',
133
+ message: 'Gateway start failed after dependencies ready, will retry',
134
+ data: { error: result.message }
135
+ })
136
+ this.startRetryLoop()
137
+ }
138
+ } else {
139
+ Logger.log({
140
+ level: 'debug',
141
+ message: 'Dependencies not ready yet',
142
+ data: { deviceNative: health.deviceNative, deviceSimulator: health.deviceSimulator }
143
+ })
144
+ }
145
+ }, retryIntervalSeconds * 1000)
146
+ }
147
+
148
+ stopRetryLoop () {
149
+ if (this.retryTimer) {
150
+ clearInterval(this.retryTimer)
151
+ this.retryTimer = null
152
+ }
153
+ }
154
+
155
+ // --- Lifecycle methods ---
156
+
157
+ async start () {
158
+ if (this.state === GatewayState.RUNNING) {
159
+ Logger.log({
160
+ level: 'warn',
161
+ message: 'App Gateway is already running'
162
+ })
163
+ return { success: false, message: 'App Gateway is already running' }
164
+ }
165
+ this.stopRetryLoop()
166
+ this.state = GatewayState.STARTING
167
+ const { propertyId, deviceNativeUrl, deviceSimulatorUrl } = Application.appGateway
168
+ try {
169
+ Logger.log({
170
+ level: 'info',
171
+ message: 'Starting App Gateway service...',
172
+ data: { propertyId, deviceNativeUrl, deviceSimulatorUrl }
173
+ })
174
+ await this.ensureGatewayExists()
175
+ this.subscribeToRequestTopic()
176
+ this.startScanInterval()
177
+ this.state = GatewayState.RUNNING
178
+ this.startedAt = Date.now()
179
+ Logger.log({
180
+ level: 'info',
181
+ message: 'App Gateway service started successfully',
182
+ data: {
183
+ gatewayId: this.gatewayDevice?.id,
184
+ macAddress: this.gatewayDevice?.macAddress
185
+ }
186
+ })
187
+ return {
188
+ success: true,
189
+ message: 'App Gateway started',
190
+ gateway: this.gatewayDevice
191
+ }
192
+ } catch (error) {
193
+ this.state = GatewayState.ERROR
194
+ Logger.log({
195
+ level: 'error',
196
+ message: 'Failed to start App Gateway service',
197
+ data: { error: error.message, stack: error.stack }
198
+ })
199
+ return { success: false, message: error.message }
200
+ }
201
+ }
202
+
203
+ async stop () {
204
+ if (this.state === GatewayState.STOPPED) {
205
+ return { success: false, message: 'App Gateway is not running' }
206
+ }
207
+ Logger.log({
208
+ level: 'info',
209
+ message: 'Stopping App Gateway service...'
210
+ })
211
+ this.stopRetryLoop()
212
+ if (this.scanInterval) {
213
+ clearInterval(this.scanInterval)
214
+ this.scanInterval = null
215
+ }
216
+ this.state = GatewayState.STOPPED
217
+ this.startedAt = null
218
+ Logger.log({
219
+ level: 'info',
220
+ message: 'App Gateway service stopped'
221
+ })
222
+ return { success: true, message: 'App Gateway stopped' }
223
+ }
224
+
225
+ getStatus () {
226
+ const { propertyId, deviceNativeUrl, deviceSimulatorUrl, scanIntervalSeconds, autoStart } = Application.appGateway
227
+ return {
228
+ state: this.state,
229
+ isRunning: this.isRunning,
230
+ startedAt: this.startedAt,
231
+ uptime: this.startedAt ? Date.now() - this.startedAt : null,
232
+ gateway: this.gatewayDevice
233
+ ? {
234
+ id: this.gatewayDevice.id,
235
+ macAddress: this.gatewayDevice.macAddress,
236
+ propertyId: this.gatewayDevice.propertyId
237
+ }
238
+ : null,
239
+ dependencies: this.dependencyHealth,
240
+ config: {
241
+ propertyId,
242
+ deviceNativeUrl,
243
+ deviceSimulatorUrl,
244
+ scanIntervalSeconds,
245
+ autoStart
246
+ }
247
+ }
248
+ }
249
+
250
+ async triggerScan () {
251
+ if (!this.isRunning) {
252
+ return { success: false, message: 'App Gateway is not running' }
253
+ }
254
+ try {
255
+ await this.publishScanNotification()
256
+ return { success: true, message: 'Scan notification published' }
257
+ } catch (error) {
258
+ return { success: false, message: error.message }
259
+ }
260
+ }
261
+
262
+ // --- Gateway registration ---
263
+
264
+ async ensureGatewayExists () {
265
+ const { propertyId, deviceNativeUrl } = Application.appGateway
266
+ const macAddress = this.generateMacAddress()
267
+ const masterSecret = crypto.randomBytes(16).toString('base64')
268
+ try {
269
+ const createResponse = await fetch(`${deviceNativeUrl}/v1/devices/createAppGateway`, {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({
273
+ propertyId,
274
+ macAddress,
275
+ masterSecret,
276
+ supportedProtocols: [Protocol.dcpV2],
277
+ supportedOperations: [Operation.forwardPacket, Operation.forwardPacketV2],
278
+ supportedInterfaces: [Interface.ble],
279
+ supportedNotifications: [NotificationType.bridgeScanStatus]
280
+ })
281
+ })
282
+ const createResult = await createResponse.json()
283
+ if (createResult.success && createResult.data?.device) {
284
+ this.gatewayDevice = createResult.data.device
285
+ this.masterSecret = masterSecret
286
+ Logger.log({
287
+ level: 'info',
288
+ message: 'Created app gateway',
289
+ data: { gatewayId: this.gatewayDevice.id, macAddress: this.gatewayDevice.macAddress }
290
+ })
291
+ return
292
+ }
293
+ throw new Error(createResult.message || 'Failed to create app gateway')
294
+ } catch (error) {
295
+ if (error.message.includes('Failed to create')) {
296
+ throw error
297
+ }
298
+ throw new Error(`Failed to connect to device-native at ${deviceNativeUrl}: ${error.message}`)
299
+ }
300
+ }
301
+
302
+ generateMacAddress () {
303
+ const bytes = crypto.randomBytes(6)
304
+ bytes[0] = (bytes[0] | 0x02) & 0xfe
305
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':')
306
+ }
307
+
308
+ // --- MQTT subscription and message handling ---
309
+
310
+ subscribeToRequestTopic () {
311
+ if (!this.gatewayDevice) {
312
+ throw new Error('Gateway device not initialized')
313
+ }
314
+ const broker = Broker.getInstance()
315
+ const requestTopic = `rq/4/${this.gatewayDevice.id}`
316
+ broker.subscribe(
317
+ requestTopic,
318
+ (packet, cb) => {
319
+ this.handleRequest(packet.topic, packet.payload).catch(error => {
320
+ Logger.log({
321
+ level: 'error',
322
+ message: 'Error handling gateway request',
323
+ data: { error: error.message }
324
+ })
325
+ })
326
+ if (cb) cb()
327
+ },
328
+ () => {
329
+ Logger.log({
330
+ level: 'info',
331
+ message: `Subscribed to request topic: ${requestTopic}`
332
+ })
333
+ }
334
+ )
335
+ }
336
+
337
+ // --- Scan notifications ---
338
+
339
+ startScanInterval () {
340
+ const { scanIntervalSeconds } = Application.appGateway
341
+ this.publishScanNotification().catch(error => {
342
+ Logger.log({
343
+ level: 'error',
344
+ message: 'Error publishing initial scan notification',
345
+ data: { error: error.message }
346
+ })
347
+ })
348
+ this.scanInterval = setInterval(async () => {
349
+ try {
350
+ await this.publishScanNotification()
351
+ } catch (error) {
352
+ Logger.log({
353
+ level: 'error',
354
+ message: 'Error publishing scan notification',
355
+ data: { error: error.message }
356
+ })
357
+ }
358
+ }, scanIntervalSeconds * 1000)
359
+ }
360
+
361
+ async publishScanNotification () {
362
+ if (!this.gatewayDevice || !this.masterSecret) {
363
+ return
364
+ }
365
+ const { propertyId, deviceSimulatorUrl } = Application.appGateway
366
+ let scanResults = []
367
+ try {
368
+ const response = await fetch(`${deviceSimulatorUrl}/v1/devices/scanProperty`, {
369
+ method: 'POST',
370
+ headers: { 'Content-Type': 'application/json' },
371
+ body: JSON.stringify({ propertyId })
372
+ })
373
+ const result = await response.json()
374
+ if (result.success && result.data?.scanResults) {
375
+ scanResults = result.data.scanResults
376
+ }
377
+ } catch (error) {
378
+ Logger.log({
379
+ level: 'debug',
380
+ message: 'Failed to scan property',
381
+ data: { error: error.message }
382
+ })
383
+ return
384
+ }
385
+ if (!scanResults.length) {
386
+ Logger.log({
387
+ level: 'debug',
388
+ message: 'No devices found during scan'
389
+ })
390
+ return
391
+ }
392
+ const notification = new BridgeScanStatusNotification({
393
+ scanResultRecords: scanResults.map(({ rssi, macAddress, bcpPacket }) => {
394
+ const bcpPacketBuffer = Buffer.from(bcpPacket, 'base64')
395
+ return new BridgeScanResultRecord({
396
+ macAddress,
397
+ rssi: rssi || -50,
398
+ bcpPacket: bcpPacketBuffer,
399
+ timeSinceScannedSeconds: 10
400
+ })
401
+ }),
402
+ recordCount: scanResults.length,
403
+ rssi: -50
404
+ })
405
+ const transport = new DcpV2DeviceNotificationPacket({
406
+ notificationPayload: notification.serialize(),
407
+ notificationType: NotificationType.bridgeScanStatus
408
+ })
409
+ const key = Buffer.from(this.masterSecret, 'base64')
410
+ const encryptedPacket = transport.serialize({ key })
411
+ const notificationTopic = `nf/4/${this.gatewayDevice.id}`
412
+ const broker = Broker.getInstance()
413
+ broker.publish({
414
+ topic: notificationTopic,
415
+ payload: encryptedPacket,
416
+ qos: 1,
417
+ retain: true
418
+ })
419
+ await this.forwardToPubSub(notificationTopic, encryptedPacket)
420
+ Logger.log({
421
+ level: 'debug',
422
+ message: 'Published scan notification',
423
+ data: { deviceCount: scanResults.length, topic: notificationTopic }
424
+ })
425
+ }
426
+
427
+ // --- Request handling ---
428
+
429
+ async handleRequest (topic, payload) {
430
+ if (!this.gatewayDevice || !this.masterSecret) {
431
+ return
432
+ }
433
+ const { propertyId, deviceSimulatorUrl } = Application.appGateway
434
+ const key = Buffer.from(this.masterSecret, 'base64')
435
+ const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
436
+ let operationId, operationPayload, requestId
437
+ try {
438
+ const deserialized = DcpV2OnlineServerRequestPacket.deserialize({ buffer, keys: [key] })
439
+ operationId = deserialized.operationId
440
+ operationPayload = deserialized.operationPayload
441
+ requestId = deserialized.requestId
442
+ } catch (error) {
443
+ Logger.log({
444
+ level: 'error',
445
+ message: 'Failed to deserialize request packet',
446
+ data: { error: error.message }
447
+ })
448
+ return
449
+ }
450
+ const responseTopic = `rs/4/${this.gatewayDevice.id}`
451
+ let transportPacketRaw
452
+ if (operationId === Operation.forwardPacket) {
453
+ const { destinationDeviceMacAddress, packet } = ForwardPacketRequest.deserialize({ buffer: operationPayload })
454
+ try {
455
+ const response = await fetch(`${deviceSimulatorUrl}/v1/devices/processRequest`, {
456
+ method: 'POST',
457
+ headers: { 'Content-Type': 'application/json' },
458
+ body: JSON.stringify({
459
+ propertyId,
460
+ macAddress: destinationDeviceMacAddress,
461
+ requestPacket: packet.toString('base64')
462
+ })
463
+ })
464
+ const result = await response.json()
465
+ if (result.success && result.data?.responsePacket) {
466
+ const forwardPacketResponse = new ForwardPacketResponse({
467
+ packet: Buffer.from(result.data.responsePacket, 'base64')
468
+ })
469
+ transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
470
+ requestId,
471
+ responsePayload: forwardPacketResponse.serialize()
472
+ })
473
+ } else {
474
+ transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
475
+ requestId,
476
+ errorCode: ErrorCode.cannotFindDevice
477
+ })
478
+ }
479
+ } catch (error) {
480
+ Logger.log({
481
+ level: 'error',
482
+ message: 'Failed to process forward packet request',
483
+ data: { error: error.message, macAddress: destinationDeviceMacAddress }
484
+ })
485
+ transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
486
+ requestId,
487
+ errorCode: ErrorCode.cannotFindDevice
488
+ })
489
+ }
490
+ } else if (operationId === Operation.forwardPacketV2) {
491
+ const { destinationDeviceMacAddress, packet } = ForwardPacketV2Request.deserialize({ buffer: operationPayload })
492
+ try {
493
+ const response = await fetch(`${deviceSimulatorUrl}/v1/devices/processRequest`, {
494
+ method: 'POST',
495
+ headers: { 'Content-Type': 'application/json' },
496
+ body: JSON.stringify({
497
+ propertyId,
498
+ macAddress: destinationDeviceMacAddress,
499
+ requestPacket: packet.toString('base64')
500
+ })
501
+ })
502
+ const result = await response.json()
503
+ if (result.success && result.data?.responsePacket) {
504
+ const forwardPacketResponse = new ForwardPacketV2Response({
505
+ packet: Buffer.from(result.data.responsePacket, 'base64'),
506
+ remainingAttemptCount: 2
507
+ })
508
+ transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
509
+ requestId,
510
+ responsePayload: forwardPacketResponse.serialize()
511
+ })
512
+ } else {
513
+ transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
514
+ requestId,
515
+ errorCode: ErrorCode.cannotFindDevice
516
+ })
517
+ }
518
+ } catch (error) {
519
+ Logger.log({
520
+ level: 'error',
521
+ message: 'Failed to process forward packet v2 request',
522
+ data: { error: error.message, macAddress: destinationDeviceMacAddress }
523
+ })
524
+ transportPacketRaw = new DcpV2DeviceResponseForServerPacket({
525
+ requestId,
526
+ errorCode: ErrorCode.cannotFindDevice
527
+ })
528
+ }
529
+ } else {
530
+ Logger.log({
531
+ level: 'warn',
532
+ message: 'Unhandled operation type',
533
+ data: { operationId }
534
+ })
535
+ return
536
+ }
537
+ const encryptedResponse = transportPacketRaw.serializeWithAes128Gcm({ key })
538
+ const broker = Broker.getInstance()
539
+ broker.publish({
540
+ topic: responseTopic,
541
+ payload: encryptedResponse,
542
+ qos: 1,
543
+ retain: true
544
+ })
545
+ await this.forwardToPubSub(responseTopic, encryptedResponse)
546
+ Logger.log({
547
+ level: 'debug',
548
+ message: 'Published response',
549
+ data: { topic: responseTopic, operationId }
550
+ })
551
+ }
552
+
553
+ // --- Pub/Sub forwarding ---
554
+
555
+ async forwardToPubSub (topicName, payload) {
556
+ const { pubsub } = Application
557
+ const messageData = {
558
+ clientId: this.gatewayDevice?.id || 'app-gateway',
559
+ topicName,
560
+ packet: payload.toString('base64'),
561
+ receivedAtMilliseconds: Date.now()
562
+ }
563
+ try {
564
+ PublishMessages({
565
+ projectId: pubsub.projectId,
566
+ topicId: 'mqttMessageReceived',
567
+ messages: [{
568
+ data: Buffer.from(JSON.stringify(messageData)).toString('base64'),
569
+ attributes: { traceId: uuid() }
570
+ }]
571
+ })
572
+ } catch (error) {
573
+ Logger.log({
574
+ level: 'error',
575
+ message: 'Failed to forward to Pub/Sub',
576
+ data: { error: error.message, topicName }
577
+ })
578
+ }
579
+ }
580
+ }
581
+
582
+ export const AppGatewayService = new AppGatewayServiceClass()